From 9d22beff887212b7c0555bcae6ad4d354312e36b Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:13:24 -0400 Subject: [PATCH 1/9] fix(tools): tighten four tool definitions below 4.8 and finish three 5.0 tools - vault_search_by_property: one numeric-matching bullet in place of three - vault_find_orphans: one example; shorter daily-notes default sentence - vault_delete_note: shorter Errors entries, each keeping its remedy - vault_search_by_tag: route past the 20-result cap; drop a schema restatement - vault_get_backlinks: Errors as bullets quoting the message - vault_recent_notes: additional_properties marked optional - vault_list_notes: folder path-error entry cut to its remedy - Error-contract tests for vault_list_notes and vault_list_files folder errors Co-Authored-By: Claude Opus 5.5 --- .../server-error-contracts.test.ts | 78 +++++++++++++++++++ .../__snapshots__/tool-surface/default.json | 14 ++-- .../tool-surface/disabled-tools.json | 14 ++-- .../tool-surface/embedding-off.json | 14 ++-- .../file-tools-off+embedding-off.json | 14 ++-- .../tool-surface/file-tools-off.json | 14 ++-- .../memory-off+embedding-off.json | 14 ++-- ...mory-off+file-tools-off+embedding-off.json | 14 ++-- .../memory-off+file-tools-off.json | 14 ++-- .../tool-surface/memory-off.json | 14 ++-- .../tool-surface/obsidian-sync.json | 14 ++-- .../tool-surface/readonly+embedding-off.json | 12 +-- ...readonly+file-tools-off+embedding-off.json | 12 +-- .../tool-surface/readonly+file-tools-off.json | 12 +-- .../readonly+memory-off+embedding-off.json | 12 +-- ...mory-off+file-tools-off+embedding-off.json | 12 +-- .../readonly+memory-off+file-tools-off.json | 12 +-- .../tool-surface/readonly+memory-off.json | 12 +-- .../__snapshots__/tool-surface/readonly.json | 12 +-- .../__tests__/tool-definitions.test.ts | 7 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 18 ++--- .../mcp-core/tools/vault-crud-tools.ts | 16 ++-- 22 files changed, 218 insertions(+), 137 deletions(-) diff --git a/src/__tests__/integration/server-error-contracts.test.ts b/src/__tests__/integration/server-error-contracts.test.ts index 618eb34d6..1c3f7cda7 100644 --- a/src/__tests__/integration/server-error-contracts.test.ts +++ b/src/__tests__/integration/server-error-contracts.test.ts @@ -254,6 +254,30 @@ describe("absolute path blocked", () => { `absolute path blocked: "${serverVaultPath}/moved.md" must be vault-relative`, ) }) + + it("vault_list_notes rejects an absolute container path as folder", async () => { + const result = await callTool({ + client, + name: "vault_list_notes", + args: { folder: `${serverVaultPath}/Projects` }, + }) + expectToolError( + result, + `absolute path blocked: "${serverVaultPath}/Projects" must be vault-relative`, + ) + }) + + it("vault_list_files rejects an absolute container path as folder", async () => { + const result = await callTool({ + client, + name: "vault_list_files", + args: { folder: `${serverVaultPath}/Projects` }, + }) + expectToolError( + result, + `absolute path blocked: "${serverVaultPath}/Projects" must be vault-relative`, + ) + }) }) // ── Path traversal ─────────────────────────────────────────── @@ -370,6 +394,42 @@ describe("path traversal blocked", () => { }) expectToolError(result, "path traversal blocked") }) + + it("vault_list_notes rejects a folder escaping the vault root", async () => { + const result = await callTool({ + client, + name: "vault_list_notes", + args: { folder: "../outside" }, + }) + expectToolError(result, 'path traversal blocked: "../outside" escapes vault root') + }) + + it("vault_list_notes rejects a folder naming the vault root", async () => { + const result = await callTool({ + client, + name: "vault_list_notes", + args: { folder: "." }, + }) + expectToolError(result, 'path traversal blocked: "." resolves to the vault root') + }) + + it("vault_list_files rejects a folder escaping the vault root", async () => { + const result = await callTool({ + client, + name: "vault_list_files", + args: { folder: "../outside" }, + }) + expectToolError(result, 'path traversal blocked: "../outside" escapes vault root') + }) + + it("vault_list_files rejects a folder naming the vault root", async () => { + const result = await callTool({ + client, + name: "vault_list_files", + args: { folder: "." }, + }) + expectToolError(result, 'path traversal blocked: "." resolves to the vault root') + }) }) // ── Hidden paths ───────────────────────────────────────────── @@ -486,6 +546,24 @@ describe("hidden path blocked", () => { }) expectToolError(result, "hidden path blocked") }) + + it("vault_list_notes rejects a hidden folder", async () => { + const result = await callTool({ + client, + name: "vault_list_notes", + args: { folder: ".obsidian" }, + }) + expectToolError(result, 'hidden path blocked: ".obsidian" targets a hidden file or folder') + }) + + it("vault_list_files rejects a hidden folder", async () => { + const result = await callTool({ + client, + name: "vault_list_files", + args: { folder: ".obsidian" }, + }) + expectToolError(result, 'hidden path blocked: ".obsidian" targets a hidden file or folder') + }) }) // ── Note not found ─────────────────────────────────────────── diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index db0408a5c..ccbb150df 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json @@ -195,7 +195,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -272,7 +272,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -307,7 +307,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -537,7 +537,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1154,7 +1154,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1467,7 +1467,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1513,7 +1513,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json index 5d656c7ca..07ba496c5 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -309,7 +309,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -539,7 +539,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1096,7 +1096,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1409,7 +1409,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1455,7 +1455,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json index 8938268a3..410220646 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -309,7 +309,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -539,7 +539,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1156,7 +1156,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1469,7 +1469,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1515,7 +1515,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json index 4c7d272d5..ecbccfdde 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json @@ -198,7 +198,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -275,7 +275,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -310,7 +310,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -499,7 +499,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1072,7 +1072,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1385,7 +1385,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1431,7 +1431,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json index 023faf02b..9b57a6f09 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -309,7 +309,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -498,7 +498,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1071,7 +1071,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1384,7 +1384,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1430,7 +1430,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json index 2f52a18ed..c35cdc69b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -264,7 +264,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -440,7 +440,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1017,7 +1017,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1330,7 +1330,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1376,7 +1376,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json index 0b0e16d5e..934a022c7 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json @@ -153,7 +153,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -230,7 +230,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -265,7 +265,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -400,7 +400,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -933,7 +933,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1246,7 +1246,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1292,7 +1292,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json index 31e061855..470733b6e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -264,7 +264,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -399,7 +399,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -932,7 +932,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1245,7 +1245,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1291,7 +1291,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json index ba41b704a..815d792aa 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ already holds this name and its numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old trash copies, then retry\n- any other \"cannot move to trash …\" — the .trash/ move failed (e.g. a plain file blocks a needed folder); the note stays put; fix .trash/, then retry\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable; the delete is blocked because a guessed setting could let the retention sweep remove a note set to be kept forever; repair the file, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but is unreadable, and a guessed setting could let the retention sweep remove a keep-forever note; repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -263,7 +263,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -439,7 +439,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1016,7 +1016,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1329,7 +1329,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1375,7 +1375,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json index f60cd0d1c..56495551b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n- any other \"cannot delete …\" — the permanent delete failed (e.g. permissions); the note stays put; fix the cause, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Unless the server syncs through Obsidian Sync, the \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. The server deletes its own copies there after its TRASH_RETENTION_DAYS setting (default 30 days, or never when set to none), never touching notes Obsidian trashed.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- When the server syncs through Obsidian Sync, the setting is bypassed and the note is always deleted for good; recover it from Sync's version history (1 month on Standard, 12 months on Plus).\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note 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\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- any other \"cannot delete …\" — the note stays put; fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the protected daily notes folder is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash ()\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -309,7 +309,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -539,7 +539,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -1156,7 +1156,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -1469,7 +1469,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1515,7 +1515,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json index 50191f640..262f428b6 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -44,7 +44,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -220,7 +220,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -738,7 +738,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -957,7 +957,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1003,7 +1003,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json index 32beefa85..b00cbc5b1 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -45,7 +45,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -180,7 +180,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -654,7 +654,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -873,7 +873,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -919,7 +919,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json index 7e6463f9e..5dabaca2d 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -44,7 +44,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -179,7 +179,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -653,7 +653,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -872,7 +872,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -918,7 +918,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json index d35035218..9af385481 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -45,7 +45,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -167,7 +167,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -645,7 +645,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -864,7 +864,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -910,7 +910,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json index 034bf1805..a4ddbe2f1 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -46,7 +46,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -127,7 +127,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -561,7 +561,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -780,7 +780,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -826,7 +826,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json index f3d563a18..8b97bc448 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -45,7 +45,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -126,7 +126,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -560,7 +560,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -779,7 +779,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -825,7 +825,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json index c02895113..60d6b265d 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -44,7 +44,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -166,7 +166,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -644,7 +644,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -863,7 +863,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -909,7 +909,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json index 8ef0045ef..36e3ea833 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Archive\"], limit: 10 })\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- The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → \"Daily Notes\"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses \"Daily Notes\".\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\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- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.\n- exclude_folders replaces the defaults (including an environment override), it does not add to them — include the defaults yourself to keep them. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\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": { @@ -43,7 +43,7 @@ { "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.", + "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:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", "inputSchema": { "type": "object", "properties": { @@ -219,7 +219,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". This tool reads the filesystem rather than the search index, so use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"*.md\" lists the folder's top-level notes and \"**/*.md\" every note under it. Returned paths are always vault-relative.\n\nBehavior: Paths come back sorted by vault-relative path, uppercase before lowercase. Hidden (dot-prefixed) notes and folders are never listed, matching Obsidian; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -737,7 +737,7 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery. 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.", + "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": { @@ -956,7 +956,7 @@ { "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).\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 is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\".\n- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like \"4abc\", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text.\n- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as \"1e-999\" matches stored zero.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", + "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).\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 and text values match exactly and case-sensitively, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Stored precision applies: large integers can round together, and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" or \"0\" (true is stored as 1, false as 0); \"1.0\" does not match a checked checkbox.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit applies after sorting. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.", "inputSchema": { "type": "object", "properties": { @@ -1002,7 +1002,7 @@ { "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\". exact=true matches 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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "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\" })\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no 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 + exact interact: the prefix match follows the \"/\" separator, so \"project\" matches itself and its children but does NOT match \"my-project\" or \"projects\".\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 note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately, or list every note with one exact tag via vault_search_by_property({ key: \"tags\", value: \"\", limit }). bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 00943f8de..703cfc446 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -1635,8 +1635,11 @@ describe("vault_find_orphans live folder defaults", () => { it("describes live sources and states the exclusion default in the schema", async () => { const { toolConfig } = await setupOrphans({ settings: '{"folder":"Journal"}' }) - expect(toolConfig.description).toContain( - 'The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → "Daily Notes")', + const defaultsLine = toolConfig.description + ?.split("\n") + .find((line) => line.startsWith("- The daily notes folder")) + expect(defaultsLine).toBe( + '- The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.', ) expect(toolConfig.description).not.toContain("Journal") expect(toolConfig.inputSchema?.exclude_folders?.description).toBe( diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 42845223c..26851e522 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -160,12 +160,12 @@ When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no tex Prefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags. Parameters: -- tag + exact interact: the prefix match follows the "/" separator, so "project" matches itself and its children but does NOT match "my-project" or "projects". exact=true matches only the literal tag, excluding children. +- tag + exact interact: the prefix match follows the "/" separator, so "project" matches itself and its children but does NOT match "my-project" or "projects". Errors: - An unknown tag or no matches returns an empty array, not an error — don't use as an existence check. -Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.`, +Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified and capped at 20, with no truncation signal: exactly 20 results may mean more exist. There is no offset: query each child tag separately${whenToolEnabledText("vault_search_by_property", ', or list every note with one exact tag via vault_search_by_property({ key: "tags", value: "", limit })')}. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.`, inputSchema: { tag: z .string() @@ -250,7 +250,7 @@ Parameters: Errors: - An empty vault returns an empty array, not an error. -Returns: 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.`, +Returns: 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: { sort_by: z .enum(["created", "modified"]) @@ -453,9 +453,8 @@ When to use: Finding notes by metadata when you don't have a text query. Prefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes. Parameters: -- key is exact and case-sensitive. Text values match exactly and case-sensitively, with no partial matching or globbing. Stored numbers also match numerically: "04" and "4.0" match number 4 and their own literal text, but not text "4". -- Numeric matching accepts complete finite YAML core numeric forms: signed decimals, leading-zero decimals, .5, 4., exponents, 0x hexadecimal and 0o octal. Whitespace, final line breaks, prefixes like "4abc", comments, expressions, 0b binary, separators, non-finite values and overflow match only literal text. -- Numeric equality uses stored number precision: large integers can round to the same value, and underflow such as "1e-999" matches stored zero. +- key and text values match exactly and case-sensitively, with no partial matching or globbing. +- A value written as a complete, finite YAML number (signed or leading-zero decimals, .5, 4., exponents, 0x, 0o) also matches stored numbers numerically: "04" and "4.0" match number 4 and their own literal text, but not text "4". Anything else ("4abc", " 4", 0b binary, "1e999") matches only as text. Stored precision applies: large integers can round together, and "1e-999" matches 0. - Pass a checkbox as "1" or "0" (true is stored as 1, false as 0); "1.0" does not match a checked checkbox. - An array element must equal value in full: "blog" matches tags: ["blog", "draft"] but not tags: ["my-blog"]. - folder names a whole folder and includes its subfolders: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Matching ignores ASCII letter case; omit folder to search the entire vault. @@ -518,7 +517,9 @@ Parameters: Returns: 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). -Errors: 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.`, +Errors: +- "path must end in …" — add the .md or .canvas extension +- A path not in the index returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.`, inputSchema: { path: z .string() @@ -617,7 +618,7 @@ Errors: : `daily notes folder, Templates, ${JSON.stringify(config.memoryDir)}` const orphanDefaultDescription = config.orphanExcludeFoldersOverride ? "With exclude_folders omitted, the ORPHAN_EXCLUDE_FOLDERS override is used." - : 'The daily notes folder is resolved on each call (DAILY_NOTES_FOLDER → .obsidian/daily-notes.json → "Daily Notes"). ORPHAN_EXCLUDE_FOLDERS replaces the defaults. For unreadable daily settings, the server logs a warning and uses "Daily Notes".' + : 'The daily notes folder is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). ORPHAN_EXCLUDE_FOLDERS replaces the defaults.' registerTool( TOOL_NAMES.VAULT_FIND_ORPHANS, @@ -626,7 +627,6 @@ Errors: description: `Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored). Example: vault_find_orphans({}) -Example: vault_find_orphans({ exclude_folders: ["Archive"], limit: 10 }) When to use: Vault maintenance — surfacing notes to integrate into the graph.${whenToolEnabledText("vault_patch_note", " Link an orphan by mentioning it from a relevant note with vault_patch_note.")} Prefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault. diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 4f3f79223..273dd2776 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -327,7 +327,7 @@ Behavior: Paths come back sorted by vault-relative path, uppercase before lowerc Errors: - A nonexistent folder or no glob matches returns an empty array, not an error. -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — the folder starts at the filesystem root, escapes the vault or names the vault root itself (e.g. "."), or is hidden like ".obsidian"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault. +- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative folder outside hidden folders, and omit folder to list the whole vault. Returns: JSON array of vault-relative path strings (e.g. ["Notes/idea.md", "Projects/plan.md"]).`, inputSchema: { @@ -489,7 +489,7 @@ Editing a leading callout: read it via vault_read_note(outline: true), then vaul ), ]) - // The edit tools' Errors entries keep a remedy when the tool they point to is disabled. + // The edit and delete tools' Errors entries keep a remedy when the tool they point to is disabled. const listNotesEnabled = isToolEnabled("vault_list_notes") const noteNotFoundCheckRemedy = listNotesEnabled ? "check vault_list_notes for valid paths" @@ -1037,12 +1037,12 @@ Returns: Confirmation message "Inserted lines anchor in