From 05a1032233e08f1a70ce415e44725ca918a34849 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:10:16 -0400 Subject: [PATCH 01/12] fix(search): compare frontmatter tags ignoring letter case and store them by Obsidian's tag rule Tag comparisons across the index now fold both sides with toLowerCase(), the rule Obsidian's Tags view groups spellings by: vault_search_by_tag in both modes, vault_search's tags filter, vault_list_tasks's tag filter, and vault_list_tags, which merges spellings under the most-used one. A leading "#" on any tag input is accepted; an input left empty after it is rejected. vault_search's tags filter matches nested tags like vault_search_by_tag. Frontmatter tags are stored by Obsidian 1.14.4's own rule, in a new pure parser (obsidian-markdown/tags.ts): the first key spelled tags in any letter case, a text value as one tag, a list's text entries, each without a leading "#" or a trailing "/", and only names Obsidian accepts as tags. Co-Authored-By: Claude Fable 5.1 --- ARCHITECTURE.md | 3 + .../server-error-contracts.test.ts | 34 ++++ .../__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 | 14 +- ...readonly+file-tools-off+embedding-off.json | 14 +- .../tool-surface/readonly+file-tools-off.json | 14 +- .../readonly+memory-off+embedding-off.json | 14 +- ...mory-off+file-tools-off+embedding-off.json | 14 +- .../readonly+memory-off+file-tools-off.json | 14 +- .../tool-surface/readonly+memory-off.json | 14 +- .../__snapshots__/tool-surface/readonly.json | 14 +- .../__tests__/tool-definitions.test.ts | 2 +- .../tools/__tests__/tool-helpers.test.ts | 39 +++- src/vault-mcp/mcp-core/tools/search-tools.ts | 17 +- src/vault-mcp/mcp-core/tools/task-tools.ts | 3 +- src/vault-mcp/mcp-core/tools/tool-helpers.ts | 8 +- .../obsidian-markdown/__tests__/tags.test.ts | 100 ++++++++++ src/vault-mcp/obsidian-markdown/tags.ts | 58 ++++++ .../search/__tests__/hybrid-search.test.ts | 34 ++++ .../search/__tests__/search-helpers.test.ts | 100 ++++++++++ .../search/__tests__/search-index.test.ts | 187 +++++++++++++++++- .../search/__tests__/task-queries.test.ts | 29 +++ src/vault-mcp/search/search-helpers.ts | 47 ++++- src/vault-mcp/search/search-index.ts | 12 +- src/vault-mcp/search/search-queries.ts | 55 ++++-- 34 files changed, 818 insertions(+), 162 deletions(-) create mode 100644 src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts create mode 100644 src/vault-mcp/obsidian-markdown/tags.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 411ea82e2..dadd21a6b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -221,6 +221,8 @@ Both `vault_delete_note` and `vault_move_note` support `prune_empty_folders` to `vault_search_by_tag` matches a frontmatter tag and every tag nested under it (`project` matches `project/a/b` but not `projects`), or the tag alone with `exact: true`. It returns the most recently modified notes first, up to `limit` (default 20). +Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) matches nested tags the same way and ignores letter case. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. + `vault_recent_notes` sorts by `sort_by` — `"created"` or `"modified"` (default `"modified"`). ### Property discovery + daily notes @@ -234,6 +236,7 @@ Both `vault_delete_note` and `vault_move_note` support `prune_empty_folders` to **Promoted properties:** Five frontmatter keys — `title`, `tags`, `type`, `created`, `related` — get dedicated columns in the `notes` table for direct `WHERE`-clause filtering (no `json_extract` needed). In tool responses, these appear as top-level fields; remaining frontmatter keys are returned under `additional_properties` (via `formatNoteMetadata` in `tool-helpers.ts`). All other properties live in a JSON `properties` column — functional for any schema, but without dedicated columns. The property queries match a key as data through `json_each` over that column, never as a JSON path, so a property named `a.b` or `k[0]` is matched like any other. Array values are unpacked via `json_each`, so scalar and list properties both match. +- **Tags column:** holds the tags Obsidian's Tags view shows for the note, read by `obsidian-markdown/tags.ts` from the first key spelled `tags` in any letter case. A text value is one tag and a list keeps its text entries; each loses a leading `#` and a trailing `/`, and only names Obsidian accepts as tags are kept (no spaces, none of the punctuation Obsidian rejects such as `,`, `:` or `+`, and not digits only). - **Listing values:** `vault_list_property_values`, and the `sample_values` in `vault_list_property_keys`, count each value by the text the tool returns. The number `1` and the text `"1"` both return `"1"`, so they share one count; the text `"1.0"` keeps its own. Counting happens before `limit` and sample selection apply, and each item of a list property counts on its own. - **Matching a value:** `vault_search_by_property` matches text exactly. A value written as a whole, finite number in [YAML 1.2 core schema](https://yaml.org/spec/1.2.2/#1032-tag-resolution) form (`4`, `-0.5`, `1e3`, `0x1F`, `0o17`) also matches stored numbers equal to it as [JavaScript numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_encoding). So `"04"` matches the number `4` and the text `"04"`, but not the text `"4"`. Checkbox values match only `"1"` and `"0"`. diff --git a/src/__tests__/integration/server-error-contracts.test.ts b/src/__tests__/integration/server-error-contracts.test.ts index 84527a60b..1fb674a7e 100644 --- a/src/__tests__/integration/server-error-contracts.test.ts +++ b/src/__tests__/integration/server-error-contracts.test.ts @@ -2501,3 +2501,37 @@ describe("cannot read daily notes config", () => { }) }, 30_000) }) + +// ── Empty tag inputs ───────────────────────────────────────── + +describe("tag must not be empty", () => { + const EMPTY_TAG_MESSAGE = '[Error]: tag must not be empty after its leading "#"' + + it("vault_search_by_tag rejects a tag that is only a #", async () => { + const result = await callTool({ client, name: "vault_search_by_tag", args: { tag: "#" } }) + expect(result).toEqual({ + content: [{ type: "text", text: EMPTY_TAG_MESSAGE }], + isError: true, + }) + }) + + it("vault_search rejects a tags filter entry that is only a #", async () => { + const result = await callTool({ + client, + name: "vault_search", + args: { query: "note", filters: { tags: ["#"] } }, + }) + expect(result).toEqual({ + content: [{ type: "text", text: EMPTY_TAG_MESSAGE }], + isError: true, + }) + }) + + it("vault_list_tasks rejects a tag that is only a #", async () => { + const result = await callTool({ client, name: "vault_list_tasks", args: { tag: "#" } }) + expect(result).toEqual({ + content: [{ type: "text", text: EMPTY_TAG_MESSAGE }], + isError: true, + }) + }) +}) 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 0c1f57144..3afc7c5fd 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 @@ -631,7 +631,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -650,7 +650,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -834,7 +834,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1283,7 +1283,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1302,7 +1302,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1512,14 +1512,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 6f7c45302..9d0e6fd29 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1225,7 +1225,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1244,7 +1244,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1454,14 +1454,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 55716d394..528e5db2d 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1304,7 +1304,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1514,14 +1514,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 c9f2c21da..95a2175b8 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 @@ -593,7 +593,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -612,7 +612,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -796,7 +796,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1201,7 +1201,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1220,7 +1220,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1430,14 +1430,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 732186f54..44a3af854 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 @@ -592,7 +592,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -611,7 +611,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -795,7 +795,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1200,7 +1200,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1219,7 +1219,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1429,14 +1429,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 c1d123826..03f8c07f5 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 @@ -534,7 +534,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -553,7 +553,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -737,7 +737,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1146,7 +1146,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1165,7 +1165,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1375,14 +1375,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 e4988a82b..e48478478 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 @@ -494,7 +494,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -513,7 +513,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -697,7 +697,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1062,7 +1062,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1081,7 +1081,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1291,14 +1291,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 8eaf6a8db..c3f24a0cc 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 @@ -493,7 +493,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -512,7 +512,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -696,7 +696,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1061,7 +1061,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1080,7 +1080,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1290,14 +1290,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 474d38018..be93fcd97 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 @@ -533,7 +533,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -552,7 +552,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -736,7 +736,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1145,7 +1145,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1164,7 +1164,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1374,14 +1374,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 608973ea0..9c14b27a1 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1304,7 +1304,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1514,14 +1514,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 ca0865ca4..61e8b00d7 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 @@ -315,7 +315,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -334,7 +334,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -518,7 +518,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -774,7 +774,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -793,7 +793,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1003,14 +1003,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json index b2e09ee15..ec6d0a060 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 @@ -275,7 +275,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -294,7 +294,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -478,7 +478,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -690,7 +690,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -709,7 +709,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -919,14 +919,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json index 411cef4df..c4f1280d3 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 @@ -274,7 +274,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -293,7 +293,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -477,7 +477,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -689,7 +689,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -708,7 +708,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -918,14 +918,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json index dd67d34f9..4ffdef4f9 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 @@ -262,7 +262,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -281,7 +281,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -465,7 +465,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -681,7 +681,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -700,7 +700,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -910,14 +910,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json index 47256201b..fdbdb5c80 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 @@ -222,7 +222,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -241,7 +241,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -425,7 +425,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -597,7 +597,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -616,7 +616,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -826,14 +826,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json index 37ad40032..9e20ef529 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 @@ -221,7 +221,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -240,7 +240,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -424,7 +424,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -596,7 +596,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -615,7 +615,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -825,14 +825,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json index a72de91b0..6f923ba6c 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 @@ -261,7 +261,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -280,7 +280,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -464,7 +464,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -680,7 +680,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -699,7 +699,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -909,14 +909,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json index a2bde8cff..a17ec7c1f 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 @@ -314,7 +314,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -333,7 +333,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -517,7 +517,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "description": "Inline task tag; parent tags match children, letter case ignored", "type": "string", "minLength": 1 }, @@ -773,7 +773,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -792,7 +792,7 @@ "minLength": 1 }, "tags": { - "description": "Require all listed tags (AND — every tag must be present)", + "description": "Tags to match (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1002,14 +1002,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"#project\", \"project/\", and \"Project\" do not match notes tagged \"project\"; drop the \"#\" and the trailing \"/\", and use the tag's letter case.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 5c858594a..0839d6fa9 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -2742,7 +2742,7 @@ describe("DISABLED_TOOLS", () => { "When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query." const TAG_SEARCH_LIST_TAGS_SENTENCE = " Use vault_list_tags first to discover available tags." const TAG_SEARCH_SEARCH_LINE = - "\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags." + "\nPrefer vault_search when you also need text-based relevance ranking." it.each([ { diff --git a/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts b/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts index da23213dc..ae8980c50 100644 --- a/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts +++ b/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts @@ -6,7 +6,7 @@ import { UnsupportedPropertiesBlockError, } from "../../../obsidian-markdown/frontmatter.js" import type { ToolName } from "../../tool-registry.js" -import { createSafeHandlers, describeTextWindow } from "../tool-helpers.js" +import { createSafeHandlers, describeTextWindow, formatNoteMetadata } from "../tool-helpers.js" /** A handler failure whose cause carries detail the client must never see. */ const failWithCause = async (): Promise => { @@ -407,3 +407,40 @@ describe("describeTextWindow", () => { ).toBe("full.md — lines 1–1 of 1 (end of file)") }) }) + +describe("formatNoteMetadata", () => { + it("keeps only unpromoted keys under additional_properties", () => { + const formatted = formatNoteMetadata({ + path: "a.md", + title: "A", + tags: ["x"], + leading_callout: null, + properties: { + title: "A", + tags: ["x"], + type: "note", + created: "2026-01-01", + related: [], + topic: "tags", + }, + }) + + expect(formatted).toEqual({ + path: "a.md", + title: "A", + tags: ["x"], + additional_properties: { topic: "tags" }, + }) + }) + + it("treats a tags key in any letter case as promoted, so a Tags: note reports its tags once", () => { + const formatted = formatNoteMetadata({ + path: "a.md", + tags: ["x"], + leading_callout: null, + properties: { Tags: ["x"], TAGS: ["y"] }, + }) + + expect(formatted).toEqual({ path: "a.md", tags: ["x"] }) + }) +}) diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 9e02832e1..0371be810 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -26,6 +26,7 @@ export const registerSearchTools = ({ Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case +- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -41,12 +42,14 @@ Errors: - No matches returns { results: [], total: 0 }, not an error - Malformed query syntax is sanitized automatically — the tool never throws a query syntax error - A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") +- "tag must not be empty after its leading "#"" — a tags entry is "#" alone; pass the tag name Returns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode ("hybrid" or "fts"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — "hybrid" when vector embeddings contributed, "fts" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is "note" for markdown notes or "file" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".canvas", ".pdf", ".txt"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.` : `Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the "narrow by metadata, search by text" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically. Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case +- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -62,6 +65,7 @@ Errors: - No matches returns { results: [], total: 0 }, not an error - Malformed query syntax is sanitized automatically — the tool never throws a query syntax error - A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") +- "tag must not be empty after its leading "#"" — a tags entry is "#" alone; pass the tag name Returns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode ("fts" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is "note" for markdown notes or "file" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".canvas", ".pdf", ".txt"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.`, inputSchema: { @@ -72,7 +76,7 @@ Returns: JSON with results array (path, title, snippet, score, tags, folder, typ tags: z .array(z.string().min(1)) .optional() - .describe("Require all listed tags (AND — every tag must be present)"), + .describe('Tags to match (e.g. ["reference"])'), related: z .array(z.string().min(1)) .optional() @@ -153,12 +157,12 @@ Returns: JSON with results array (path, title, snippet, score, tags, folder, typ TOOL_NAMES.VAULT_SEARCH_BY_TAG, { title: "Search by Tag", - description: `Find notes by frontmatter tag; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: "project" matches "project/vault-cortex" and "project/a/b". A nested tag continues with "/", so "project" never matches "projects" or "my-project". + description: `Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: "project" matches "project/vault-cortex" and "project/a/b". A nested tag continues with "/", so "project" never matches "projects" or "my-project". Example: vault_search_by_tag({ tag: "project" }) Example: vault_search_by_tag({ tag: "project", exact: true, limit: 50 }) — notes tagged "project" itself, up to 50 -When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query.${whenToolEnabledText("vault_list_tags", " Use vault_list_tags first to discover available tags.")}${whenToolEnabledText("vault_search", "\nPrefer vault_search when you also need text-based relevance ranking; its tags filter matches the exact tag only, without nested tags.")} +When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query.${whenToolEnabledText("vault_list_tags", " Use vault_list_tags first to discover available tags.")}${whenToolEnabledText("vault_search", "\nPrefer vault_search when you also need text-based relevance ranking.")} Parameters: - limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check. @@ -167,7 +171,8 @@ Behavior: Reads the search index, which picks up a file change within a few seco Errors: - An unknown tag or no matches returns an empty array, not an error. -- "#project", "project/", and "Project" do not match notes tagged "project"; drop the "#" and the trailing "/", and use the tag's letter case. +- "project/" does not match notes tagged "project"; drop the trailing "/". +- "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name. Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { @@ -175,7 +180,7 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce .string() .min(1) .describe( - 'Tag name without "#" prefix (e.g. "project", "session-log"). Hierarchical tags use "/" separators (e.g. "project/vault-cortex").', + 'Tag name (e.g. "project", "session-log"). Hierarchical tags use "/" separators (e.g. "project/vault-cortex").', ), exact: z .boolean() @@ -212,7 +217,7 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce TOOL_NAMES.VAULT_LIST_TAGS, { title: "List Tags", - description: `List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. "project/vault-cortex") appears as one full entry, not split into segments. Count is unique notes, not occurrences. + description: `List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. "project/vault-cortex") appears as one full entry, not split into segments. Count is unique notes, not occurrences. Example: vault_list_tags() returns [{ tag: "session-log", count: 42 }, { tag: "project/vault-cortex", count: 8 }, ...] diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index 31ad41d6d..835738852 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -45,6 +45,7 @@ Parameters: Errors: - A malformed or calendar-invalid date filter throws with remediation text ("Use YYYY-MM-DD") - path without the ".md" extension is rejected +- "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error Returns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status "done" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.`, @@ -81,7 +82,7 @@ Returns: JSON { total, tasks }. Every task carries path, line, status, status_ch .string() .min(1) .optional() - .describe('Inline task tag, bare name without "#"; parent tags match children'), + .describe("Inline task tag; parent tags match children, letter case ignored"), heading: z .union([z.string().min(1), z.array(z.string().min(1)).min(1)]) .optional() diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index 1d9d3f64c..8be384c9d 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -42,6 +42,12 @@ export type ToolRegistrationContext = ToolAvailability & // so the response doesn't contain the same data twice. const PROMOTED_KEYS = new Set(["title", "tags", "type", "created", "related"]) +// The index reads the tags from the first key spelled `tags` in any letter +// case, as Obsidian does, so a `Tags:` key is promoted too +const isPromotedKey = (key: string): boolean => { + return PROMOTED_KEYS.has(key) || key.toLowerCase() === "tags" +} + /** Reshapes NoteMetadata for client responses: keeps all top-level fields, * replaces `properties` (full frontmatter, mostly duplicated) with * `additional_properties` (only unpromoted keys like topic, agent, date). */ @@ -54,7 +60,7 @@ export const formatNoteMetadata = (meta: { const { properties, leading_callout: leadingCallout, ...fields } = meta const additional_properties = Object.fromEntries( - Object.entries(properties).filter(([key]) => !PROMOTED_KEYS.has(key)), + Object.entries(properties).filter(([key]) => !isPromotedKey(key)), ) return { diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts new file mode 100644 index 000000000..6ab4525b9 --- /dev/null +++ b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest" +import { isObsidianTagName, parseFrontmatterTags } from "../tags.js" + +describe("isObsidianTagName", () => { + it.each([ + { label: "letters", name: "project" }, + { label: "letters with digits", name: "y1984" }, + { label: "a nested path", name: "project/vault-cortex" }, + { label: "underscore and hyphen", name: "my_tag-2" }, + { label: "accented letters and an emoji", name: "émoji✨" }, + { label: "a name ending in a slash", name: "foo/" }, + ])("accepts $label", ({ name }) => { + expect(isObsidianTagName(name)).toBe(true) + }) + + it.each([ + { label: "an empty name", name: "" }, + { label: "a space", name: "a b" }, + { label: "a comma", name: "alpha,beta" }, + { label: "a plus sign", name: "c++" }, + { label: "a colon", name: "a:b" }, + { label: "a hash inside the name", name: "#x" }, + { label: "digits only", name: "1984" }, + { label: "a zero-width joiner, which sits in the General Punctuation block", name: "a‍b" }, + ])("rejects $label", ({ name }) => { + expect(isObsidianTagName(name)).toBe(false) + }) +}) + +describe("parseFrontmatterTags", () => { + it("reads a text value as one tag", () => { + expect(parseFrontmatterTags({ tags: "my-tag" })).toEqual(["my-tag"]) + }) + + it("reads no tag from a comma-joined text value", () => { + expect(parseFrontmatterTags({ tags: "alpha, beta" })).toEqual([]) + }) + + it("keeps the list entries Obsidian's Tags view keeps, trimmed, without a leading # or a trailing /", () => { + const tags = [ + "Project", + "#Alpha", + " padded ", + 2024, + true, + 3.5, + "a b", + "c++", + "a:b", + "##x", + "y1984", + "émoji✨", + "nested/child/", + "1984", + ] + + expect(parseFrontmatterTags({ tags })).toEqual([ + "Project", + "Alpha", + "padded", + "y1984", + "émoji✨", + "nested/child", + ]) + }) + + it("removes one trailing slash only, so a double slash leaves a slash", () => { + expect(parseFrontmatterTags({ tags: ["foo//"] })).toEqual(["foo/"]) + }) + + it("keeps a repeated tag once per occurrence", () => { + expect(parseFrontmatterTags({ tags: ["project", "Project", "project"] })).toEqual([ + "project", + "Project", + "project", + ]) + }) + + it.each([ + { label: "a mapping", tags: { project: true } }, + { label: "a number", tags: 2024 }, + { label: "a boolean", tags: true }, + { label: "null", tags: null }, + ])("reads no tag from $label", ({ tags }) => { + expect(parseFrontmatterTags({ tags })).toEqual([]) + }) + + it("reads the tags key in any letter case", () => { + expect(parseFrontmatterTags({ Tags: ["x"] })).toEqual(["x"]) + expect(parseFrontmatterTags({ TAGS: "y" })).toEqual(["y"]) + }) + + it("reads only the first key spelled tags, even when its value is empty", () => { + expect(parseFrontmatterTags({ Tags: null, tags: ["x"] })).toEqual([]) + }) + + it("reads no tag when the note has no tags key", () => { + expect(parseFrontmatterTags({ title: "No tags" })).toEqual([]) + }) +}) diff --git a/src/vault-mcp/obsidian-markdown/tags.ts b/src/vault-mcp/obsidian-markdown/tags.ts new file mode 100644 index 000000000..5e171f2f2 --- /dev/null +++ b/src/vault-mcp/obsidian-markdown/tags.ts @@ -0,0 +1,58 @@ +// ── Frontmatter tags, read the way Obsidian's Tags view reads them ────────── + +/** + * Matches a tag name Obsidian's Tags view accepts: no whitespace, no + * character from the General Punctuation (U+2000–U+206F) or Supplemental + * Punctuation (U+2E00–U+2E7F) blocks, and none of the listed ASCII + * punctuation, so `_`, `-`, `/`, letters, digits, emoji and other Unicode + * pass. A copy of the class Obsidian 1.14.4 tests every tag against (its + * `getTags` and property editor), without the leading `#`. + */ +const TAG_NAME_RE = /^[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~[\]\\\s]+$/ + +/** Matches a name made of ASCII digits only, which Obsidian refuses as a tag + * (`#1984` is not a tag, `#y1984` is), with the same `\d` Obsidian uses. */ +const DIGITS_ONLY_RE = /^\d+$/ + +/** Matches the `tags` property key in any letter case, as Obsidian looks it up. */ +const TAGS_KEY_RE = /^tags$/i + +/** True for a name that Obsidian's Tags view would list as a tag. */ +export const isObsidianTagName = (name: string): boolean => + TAG_NAME_RE.test(name) && !DIGITS_ONLY_RE.test(name) + +/** The raw `tags` value under the first key spelled `tags` in any letter case, + * which is the key Obsidian reads, even when its value is empty. */ +const findTagsValue = (frontmatter: Record): unknown => { + const tagsKey = Object.keys(frontmatter).find((key) => TAGS_KEY_RE.test(key)) + return tagsKey ? frontmatter[tagsKey] : undefined +} + +/** The text entries of a `tags` value, each trimmed: a text value is one + * entry, a list keeps its text elements, and any other value has none. */ +const textEntriesOf = (value: unknown): string[] => { + if (typeof value === "string") return [value.trim()] + if (Array.isArray(value)) { + return value.filter((element) => typeof element === "string").map((element) => element.trim()) + } + return [] +} + +/** The tag name an entry carries: one leading `#` and one trailing `/` + * removed, as Obsidian removes them before testing the name. */ +const tagNameOf = (entry: string): string => { + const withoutHash = entry.startsWith("#") ? entry.slice(1) : entry + return withoutHash.endsWith("/") ? withoutHash.slice(0, -1) : withoutHash +} + +/** + * The tags Obsidian's Tags view shows for a note's properties, as bare names + * in document order, one per occurrence. Reproduces Obsidian 1.14.4's + * `parseFrontMatterTags` followed by `getTags`'s name test: + * - the first key spelled `tags` in any letter case is read; + * - a text value is one entry and a list keeps its text elements, trimmed; + * - one leading `#` and one trailing `/` are removed; + * - a name is kept only if `isObsidianTagName` accepts it. + */ +export const parseFrontmatterTags = (frontmatter: Record): string[] => + textEntriesOf(findTagsValue(frontmatter)).map(tagNameOf).filter(isObsidianTagName) diff --git a/src/vault-mcp/search/__tests__/hybrid-search.test.ts b/src/vault-mcp/search/__tests__/hybrid-search.test.ts index 39dd679bb..f72fa3fae 100644 --- a/src/vault-mcp/search/__tests__/hybrid-search.test.ts +++ b/src/vault-mcp/search/__tests__/hybrid-search.test.ts @@ -438,6 +438,40 @@ Content about deployment costs and infrastructure. expect(paths).toEqual(["c.md"]) }) + it("applies the tag filter to vector-only results ignoring letter case and nesting", async () => { + const mockEmbedder = createHybridMockEmbedder() + const hybridIndex = createSearchIndex(":memory:", mockEmbedder) + const taggedNote = "---\ntags: [Work/standups]\n---\n\nStandup notes.\n" + + const noteASourceVersion = hybridIndex.upsertNote( + { filePath: "a.md", rawContent: NOTE_A, fileStat: testStat(1000) }, + logger, + ) + const taggedSourceVersion = hybridIndex.upsertNote( + { filePath: "tagged.md", rawContent: taggedNote, fileStat: testStat(1000) }, + logger, + ) + await hybridIndex.embedNote( + { sourceVersion: noteASourceVersion, notePath: "a.md", rawContent: NOTE_A }, + logger, + ) + await hybridIndex.embedNote( + { sourceVersion: taggedSourceVersion, notePath: "tagged.md", rawContent: taggedNote }, + logger, + ) + + // The query matches no indexed word, so only the vector leg can return + // the note and only the JavaScript mirror of the filter can keep it + const query = "zzqxv" + expect(hybridIndex.fullTextSearch({ query, filters: { tags: ["WORK"] } }, logger)).toEqual([]) + + const { results } = await hybridIndex.hybridSearch( + { query, filters: { tags: ["WORK"] } }, + logger, + ) + expect(results.map((result) => result.path)).toEqual(["tagged.md"]) + }) + it("applies type filter to vector-only results", async () => { const mockEmbedder = createHybridMockEmbedder() const hybridIndex = createSearchIndex(":memory:", mockEmbedder) diff --git a/src/vault-mcp/search/__tests__/search-helpers.test.ts b/src/vault-mcp/search/__tests__/search-helpers.test.ts index a1021ee53..ab5187ff1 100644 --- a/src/vault-mcp/search/__tests__/search-helpers.test.ts +++ b/src/vault-mcp/search/__tests__/search-helpers.test.ts @@ -16,6 +16,10 @@ import { pathIsInFolder, dayToEpochMsRange, parseNoteForIndex, + foldTag, + normalizeTagQuery, + tagMatchesQuery, + nestedTagLikePattern, } from "../search-helpers.js" import type { NoteRow, TaskRow } from "../search-index.js" import { parseNote, UnsupportedPropertiesBlockError } from "../../obsidian-markdown/frontmatter.js" @@ -497,6 +501,82 @@ describe("fileContentRowToSearchResult", () => { }) }) +// ── Tag comparison ─────────────────────────────────────────────────── + +describe("foldTag", () => { + it("lowercases ASCII and accented letters", () => { + expect(foldTag("Project")).toBe("project") + expect(foldTag("Ärger")).toBe("ärger") + }) + + it("folds a capital sharp s to the small one, so the pair compares equal", () => { + expect(foldTag("ẞ")).toBe(foldTag("ß")) + }) + + it("keeps sharp s apart from double s, which Obsidian lists apart", () => { + expect(foldTag("Straße")).not.toBe(foldTag("STRASSE")) + }) +}) + +describe("normalizeTagQuery", () => { + it("removes one leading # and folds", () => { + expect(normalizeTagQuery("#Project")).toBe("project") + expect(normalizeTagQuery("##x")).toBe("#x") + }) + + it("keeps a trailing slash, which only a stored tag ending in a slash equals", () => { + expect(normalizeTagQuery("foo/")).toBe("foo/") + }) + + it("throws when nothing is left after the leading #", () => { + expect(() => normalizeTagQuery("#")).toThrow('tag must not be empty after its leading "#"') + }) +}) + +describe("tagMatchesQuery", () => { + it.each([ + { + label: "the tag itself in another letter case", + storedTag: "Project", + foldedQuery: "project", + }, + { label: "a nested tag", storedTag: "project/alpha", foldedQuery: "project" }, + { + label: "a nested tag in another letter case", + storedTag: "PROJECT/Alpha", + foldedQuery: "project", + }, + ])("matches $label", ({ storedTag, foldedQuery }) => { + expect(tagMatchesQuery({ storedTag, foldedQuery })).toBe(true) + }) + + it.each([ + { + label: "a longer tag without a slash boundary", + storedTag: "projects", + foldedQuery: "project", + }, + { + label: "a tag that only ends with the query", + storedTag: "my-project", + foldedQuery: "project", + }, + { label: "the parent of the query", storedTag: "project", foldedQuery: "project/alpha" }, + ])("rejects $label", ({ storedTag, foldedQuery }) => { + expect(tagMatchesQuery({ storedTag, foldedQuery })).toBe(false) + }) +}) + +describe("nestedTagLikePattern", () => { + it("folds, removes the leading #, escapes LIKE wildcards, and appends /%", () => { + expect(nestedTagLikePattern("#A_b%")).toBe("a\\_b\\%/%") + }) + + it("throws for an input that is only a #", () => { + expect(() => nestedTagLikePattern("#")).toThrow('tag must not be empty after its leading "#"') + }) +}) + // ── noteMatchesSearchFilters ───────────────────────────────────────── describe("noteMatchesSearchFilters", () => { @@ -528,6 +608,26 @@ describe("noteMatchesSearchFilters", () => { expect(noteMatchesSearchFilters(baseRow, { tags: ["project"] })).toBe(true) expect(noteMatchesSearchFilters(baseRow, { tags: ["project", "project/alpha"] })).toBe(true) expect(noteMatchesSearchFilters(baseRow, { tags: ["missing"] })).toBe(false) + expect(noteMatchesSearchFilters(baseRow, { tags: ["project", "missing"] })).toBe(false) + }) + + it("matches a tag filter ignoring letter case and a leading #", () => { + expect(noteMatchesSearchFilters(baseRow, { tags: ["PROJECT"] })).toBe(true) + expect(noteMatchesSearchFilters(baseRow, { tags: ["#Project/Alpha"] })).toBe(true) + }) + + it("matches a tag filter by a parent tag but not by a longer or child-only name", () => { + const childOnlyRow = { ...baseRow, tags: JSON.stringify(["project/alpha"]) } + + expect(noteMatchesSearchFilters(childOnlyRow, { tags: ["project"] })).toBe(true) + expect(noteMatchesSearchFilters(childOnlyRow, { tags: ["alpha"] })).toBe(false) + expect(noteMatchesSearchFilters(baseRow, { tags: ["projects"] })).toBe(false) + }) + + it("rejects a tag filter that is only a #", () => { + expect(() => noteMatchesSearchFilters(baseRow, { tags: ["#"] })).toThrow( + 'tag must not be empty after its leading "#"', + ) }) it("filters by type", () => { diff --git a/src/vault-mcp/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index f097d103f..5521ef32e 100644 --- a/src/vault-mcp/search/__tests__/search-index.test.ts +++ b/src/vault-mcp/search/__tests__/search-index.test.ts @@ -1253,6 +1253,32 @@ describe("upsertNote", () => { expect(tags).toEqual([{ tag: "single-tag", count: 1 }]) }) + it.each([ + { + label: 'a quoted "#Alpha" entry stored without the #', + frontmatter: 'tags: ["#Alpha"]', + stored: ["Alpha"], + }, + { + label: "a comma-joined text value stored as no tag", + frontmatter: "tags: alpha, beta", + stored: [], + }, + { label: "a numeric text value stored as no tag", frontmatter: "tags: 2024", stored: [] }, + { label: "a Tags key read in its own letter case", frontmatter: "Tags: [x]", stored: ["x"] }, + ])("stores $label", ({ frontmatter, stored }) => { + index.upsertNote( + { + filePath: "tagged.md", + rawContent: `---\n${frontmatter}\n---\nbody\n`, + fileStat: testStat(1000), + }, + logger, + ) + + expect(index.recentNotes({}, logger).map((note) => note.tags)).toEqual([stored]) + }) + it("exposes the status registry it was built with", () => { const statusRegistry: ReadonlyMap = new Map([ [" ", "todo"], @@ -1655,6 +1681,42 @@ describe("fullTextSearch", () => { expect(results[0]?.path).toBe("Projects/notes.md") }) + it("matches a tags filter by a parent tag in any letter case, not a longer tag", () => { + index.upsertNote( + { + filePath: "Projects/child.md", + rawContent: "---\ntags: [Project/child]\n---\n\nMeeting notes about the child\n", + fileStat: testStat(4000), + }, + logger, + ) + index.upsertNote( + { + filePath: "Projects/decoy.md", + rawContent: "---\ntags: [projects]\n---\n\nMeeting notes about the decoy\n", + fileStat: testStat(5000), + }, + logger, + ) + + const results = index.fullTextSearch( + { query: "notes", filters: { tags: ["#PROJECT"] } }, + logger, + ) + expect(results.map((result) => result.path).toSorted()).toEqual([ + "Projects/child.md", + "Projects/notes.md", + ]) + }) + + it("rejects a tags filter entry that is only a #", () => { + const searchWithEmptyTag = () => { + return index.fullTextSearch({ query: "notes", filters: { tags: ["#"] } }, logger) + } + + expect(searchWithEmptyTag).toThrow('tag must not be empty after its leading "#"') + }) + it("respects type filter", () => { const results = index.fullTextSearch({ query: "notes", filters: { type: "project" } }, logger) expect(results).toHaveLength(1) @@ -1971,10 +2033,7 @@ describe("searchByTag", () => { expect(results.map((result) => result.path)).toEqual(["b.md", "a.md"]) }) - it.each([ - { label: 'a leading "#"', tag: "#project" }, - { label: 'a trailing "/"', tag: "project/" }, - ])("matches no note tagged project when the tag has $label", ({ tag }) => { + it('matches notes tagged project when the tag has a leading "#", but not a trailing "/"', () => { index.upsertNote( { filePath: "d.md", @@ -1983,12 +2042,82 @@ describe("searchByTag", () => { }, logger, ) - // The bare tag finds d.md, so the empty result below comes from the - // "#" or "/" alone, not from a fixture without a matching note. + // The bare tag finds d.md, so the empty "/" result below comes from the + // "/" alone, not from a fixture without a matching note. const bareTagResults = index.searchByTag({ tag: "project" }, logger) expect(bareTagResults.map((result) => result.path)).toEqual(["d.md", "b.md", "a.md"]) - expect(index.searchByTag({ tag }, logger)).toEqual([]) + const hashResults = index.searchByTag({ tag: "#project" }, logger) + expect(hashResults.map((result) => result.path)).toEqual(["d.md", "b.md", "a.md"]) + expect(index.searchByTag({ tag: "project/" }, logger)).toEqual([]) + }) + + it("treats a stored tag that ends in a slash as itself and as nested under its parent", () => { + index.upsertNote( + { + filePath: "d.md", + rawContent: "---\ntags: [foo//]\n---\nbody\n", + fileStat: testStat(4000), + }, + logger, + ) + + // `foo//` is stored as `foo/`: the query `foo/` is the tag itself, the + // query `foo` is its parent, and exact mode with `foo` is neither + expect(index.searchByTag({ tag: "foo/" }, logger).map((result) => result.path)).toEqual([ + "d.md", + ]) + expect(index.searchByTag({ tag: "foo" }, logger).map((result) => result.path)).toEqual(["d.md"]) + expect(index.searchByTag({ tag: "foo", exact: true }, logger)).toEqual([]) + }) + + it("ignores letter case in both modes, with Projects and my-project as decoys", () => { + index.upsertNote( + { + filePath: "d.md", + rawContent: "---\ntags: [Project]\n---\nbody\n", + fileStat: testStat(4000), + }, + logger, + ) + index.upsertNote( + { + filePath: "e.md", + rawContent: "---\ntags: [project, Projects, my-project]\n---\nbody\n", + fileStat: testStat(5000), + }, + logger, + ) + + expect(index.searchByTag({ tag: "PROJECT" }, logger).map((result) => result.path)).toEqual([ + "e.md", + "d.md", + "b.md", + "a.md", + ]) + expect( + index.searchByTag({ tag: "PROJECT", exact: true }, logger).map((result) => result.path), + ).toEqual(["e.md", "d.md"]) + }) + + it("folds the query the way it folds stored tags, so a capital sharp s finds a small one", () => { + index.upsertNote( + { + filePath: "d.md", + rawContent: "---\ntags: [ß, Straße]\n---\nbody\n", + fileStat: testStat(4000), + }, + logger, + ) + + expect(index.searchByTag({ tag: "ẞ" }, logger).map((result) => result.path)).toEqual(["d.md"]) + expect(index.searchByTag({ tag: "STRASSE" }, logger)).toEqual([]) + }) + + it("rejects a tag that is only a #", () => { + expect(() => index.searchByTag({ tag: "#" }, logger)).toThrow( + 'tag must not be empty after its leading "#"', + ) }) it("exact match returns the tag itself, not tags nested under it", () => { @@ -2186,6 +2315,50 @@ describe("listAllTags", () => { }) }) + it("merges spellings that differ in letter case under the most-used spelling, counting notes", () => { + index.upsertNote( + { + filePath: "b.md", + rawContent: "---\ntags: [Work]\n---\nbody\n", + fileStat: testStat(3000), + }, + logger, + ) + index.upsertNote( + { + filePath: "c.md", + rawContent: "---\ntags: [WORK, WORK, WORK]\n---\nbody\n", + fileStat: testStat(4000), + }, + logger, + ) + + // `WORK` has three occurrences in one note, `work` and `Work` one each: + // the spelling follows occurrences, the count follows distinct notes + const workEntry = index + .listAllTags({}, logger) + .find((entry) => entry.tag.toLowerCase() === "work") + expect(workEntry).toEqual({ tag: "WORK", count: 3 }) + }) + + it("breaks a spelling tie by binary order and counts a note with both spellings once", () => { + index.upsertNote( + { + filePath: "b.md", + rawContent: "---\ntags: [tie, Tie]\n---\nbody\n", + fileStat: testStat(3000), + }, + logger, + ) + + // One occurrence each: `Tie` sorts before `tie` in binary order, and the + // single note carrying both counts once + const tieEntry = index + .listAllTags({}, logger) + .find((entry) => entry.tag.toLowerCase() === "tie") + expect(tieEntry).toEqual({ tag: "Tie", count: 1 }) + }) + it("adds no tag for a mapping-valued tags property", () => { const tagsBefore = index.listAllTags({}, logger) index.upsertNote( diff --git a/src/vault-mcp/search/__tests__/task-queries.test.ts b/src/vault-mcp/search/__tests__/task-queries.test.ts index d507295ee..558f484bc 100644 --- a/src/vault-mcp/search/__tests__/task-queries.test.ts +++ b/src/vault-mcp/search/__tests__/task-queries.test.ts @@ -595,6 +595,35 @@ describe("listTasks scope filters", () => { ]) }) + it("matches the tag filter ignoring letter case and a leading #, not a longer tag", () => { + const index = indexWithBoardAndPlain() + index.upsertNote( + { + filePath: "decoys.md", + rawContent: ["- [ ] Longer tag task #errands", "- [ ] Suffix tag task #my-errand"].join( + "\n", + ), + fileStat: testStat(3000), + }, + logger, + ) + + expect( + index.listTasks({ tag: "ERRAND" }, logger).tasks.map((entry) => entry.description), + ).toEqual(["Standalone task #errand"]) + expect( + index.listTasks({ tag: "#errand" }, logger).tasks.map((entry) => entry.description), + ).toEqual(["Standalone task #errand"]) + }) + + it("rejects a tag filter that is only a #", () => { + const index = indexWithBoardAndPlain() + + expect(() => index.listTasks({ tag: "#" }, logger)).toThrow( + 'tag must not be empty after its leading "#"', + ) + }) + it("treats LIKE wildcards in the tag as literal characters when matching children", () => { const index = createTestIndex() index.upsertNote( diff --git a/src/vault-mcp/search/search-helpers.ts b/src/vault-mcp/search/search-helpers.ts index 095bbccb2..a838b375b 100644 --- a/src/vault-mcp/search/search-helpers.ts +++ b/src/vault-mcp/search/search-helpers.ts @@ -38,6 +38,47 @@ export const coerceToArray = (value: unknown): string[] => { return value ? [String(value)] : [] } +// ── Tag comparison ───────────────────────────────────────────── + +/** Folds a tag for comparison with `toLowerCase()`, the rule Obsidian's Tags + * view groups spellings by. Shared by the `fold_tag` SQL function and the + * JavaScript mirror of the SQL tag filters, so both compare by one rule. */ +export const foldTag = (tag: string): string => tag.toLowerCase() + +/** + * Folds a tag input the way the index compares it: one leading `#` removed, + * then `foldTag`. Throws when nothing is left, because an empty prefix would + * match every nested tag. + */ +export const normalizeTagQuery = (input: string): string => { + const name = input.startsWith("#") ? input.slice(1) : input + + if (name === "") throw new Error('tag must not be empty after its leading "#"') + return foldTag(name) +} + +/** True when a stored tag is the folded query or nested under it. The + * JavaScript side of NESTED_TAG_PREDICATE. */ +export const tagMatchesQuery = (params: { storedTag: string; foldedQuery: string }): boolean => { + const folded = foldTag(params.storedTag) + return folded === params.foldedQuery || folded.startsWith(`${params.foldedQuery}/`) +} + +/** + * SQL predicate over a `json_each` `value` that matches the query tag itself + * or any tag nested under it, folding each stored value once; bound to + * `nestedTagLikePattern(input)`. Appending `/` to the stored value turns + * "equal or nested" into one LIKE, so `project/%` matches `project` and + * `project/x` but not `projects`. + */ +export const NESTED_TAG_PREDICATE = "fold_tag(value) || '/' LIKE ? ESCAPE '\\'" + +/** The pattern NESTED_TAG_PREDICATE binds for a tag input: normalized, its + * LIKE wildcards escaped, then `/%`. Throws like normalizeTagQuery. */ +export const nestedTagLikePattern = (input: string): string => { + return `${escapeLikeWildcards(normalizeTagQuery(input))}/%` +} + // ── Note parsing for the index ───────────────────────────────── /** @@ -315,8 +356,12 @@ export const noteMatchesSearchFilters = (note: NoteRow, filters: SearchFilters): if (filters.tags) { const noteTags = parseStringArray(note.tags) + const foldedQueries = filters.tags.map(normalizeTagQuery) + const everyQueryMatches = foldedQueries.every((foldedQuery) => { + return noteTags.some((storedTag) => tagMatchesQuery({ storedTag, foldedQuery })) + }) - if (!filters.tags.every((tag) => noteTags.includes(tag))) return false + if (!everyQueryMatches) return false } if (filters.type && note.type !== filters.type) return false diff --git a/src/vault-mcp/search/search-index.ts b/src/vault-mcp/search/search-index.ts index 467f38ed2..ff2023f43 100644 --- a/src/vault-mcp/search/search-index.ts +++ b/src/vault-mcp/search/search-index.ts @@ -13,6 +13,7 @@ import { links } from "../obsidian-markdown/links.js" import { splitIntoLines } from "../obsidian-markdown/lines.js" import { parseHeadings } from "../obsidian-markdown/headings.js" import { parseMemoryEntries, type MemoryEntry } from "../obsidian-markdown/memory-entries.js" +import { parseFrontmatterTags } from "../obsidian-markdown/tags.js" import { DEFAULT_STATUS_REGISTRY, tasks } from "../obsidian-markdown/tasks.js" import type { StatusClassification, TaskPriority, TaskStatus } from "../obsidian-markdown/tasks.js" import { contentHash, type Embedder } from "./embedder.js" @@ -30,6 +31,7 @@ import { coerceToArray, buildFtsMetadataText, escapeLikeWildcards, + foldTag, parseNoteForIndex, } from "./search-helpers.js" import * as queries from "./search-queries.js" @@ -365,6 +367,12 @@ export const createSearchIndex = ( return String(value) }) + // Tag predicates compare folded values; the same fold runs in the + // JavaScript mirror of the search filters (foldTag) + db.function("fold_tag", { deterministic: true }, (value: unknown): string => { + return foldTag(String(value)) + }) + db.pragma("journal_mode = WAL") db.pragma("synchronous = NORMAL") // better-sqlite3 already defaults sqlite3_busy_timeout to 5000 ms at open; @@ -1455,7 +1463,7 @@ export const createSearchIndex = ( unreadableBlockError, } = parseNoteForIndex(rawContent) - const tags = coerceToArray(frontmatter.tags) + const tags = parseFrontmatterTags(frontmatter) const related = coerceToArray(frontmatter.related) const bodyLines = splitIntoLines(noteBody) @@ -1681,7 +1689,7 @@ export const createSearchIndex = ( const metadataPrefix = options?.ranking?.enrichChunkMetadata ? buildChunkMetadataPrefix({ type: isString(frontmatter.type) ? frontmatter.type : null, - tags: coerceToArray(frontmatter.tags), + tags: parseFrontmatterTags(frontmatter), }) : null const chunks = chunkContent({ diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index d9d2e6bbe..7b011951d 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -18,6 +18,9 @@ import { folderLikePattern, stripTrailingSlashes, dayToEpochMsRange, + normalizeTagQuery, + nestedTagLikePattern, + NESTED_TAG_PREDICATE, } from "./search-helpers.js" import type { FileContentFtsRow } from "./search-helpers.js" import type { @@ -173,9 +176,12 @@ export const fullTextSearch = ( } if (params.filters?.tags) { + // Each listed tag matches the tag itself or any tag nested under it, as + // Obsidian's tag: search operator does; the JavaScript mirror is + // noteMatchesSearchFilters for (const tag of params.filters.tags) { - conditions.push("EXISTS (SELECT 1 FROM json_each(n.tags) WHERE value = ?)") - queryParams.push(tag) + conditions.push(`EXISTS (SELECT 1 FROM json_each(n.tags) WHERE ${NESTED_TAG_PREDICATE})`) + queryParams.push(nestedTagLikePattern(tag)) } } @@ -762,7 +768,7 @@ export const memoryRecall = async ( // ── Discovery queries ────────────────────────────────────────── -/** Finds notes with a specific tag. Supports hierarchical prefix matching. */ +/** Finds notes with a specific tag, ignoring letter case. Supports hierarchical prefix matching. */ export const searchByTag = ( context: SearchQueryContext, params: { @@ -775,12 +781,12 @@ export const searchByTag = ( const limit = Math.max(0, Math.floor(params.limit ?? 20)) const condition = params.exact - ? "EXISTS (SELECT 1 FROM json_each(n.tags) WHERE value = ?)" - : "EXISTS (SELECT 1 FROM json_each(n.tags) WHERE value = ? OR value LIKE ? || '/%' ESCAPE '\\')" + ? "EXISTS (SELECT 1 FROM json_each(n.tags) WHERE fold_tag(value) = ?)" + : `EXISTS (SELECT 1 FROM json_each(n.tags) WHERE ${NESTED_TAG_PREDICATE})` const queryParams: unknown[] = params.exact - ? [params.tag, limit] - : [params.tag, escapeLikeWildcards(params.tag), limit] + ? [normalizeTagQuery(params.tag), limit] + : [nestedTagLikePattern(params.tag), limit] const sql = ` SELECT path, title, tags, related, folder, type, created, mtime, properties, leading_callout, bytes @@ -1035,11 +1041,9 @@ export const listTasks = ( if (params.tag !== undefined) { // Same nested-tag semantics as searchByTag's prefix mode: "project" - // matches both #project and #project/vault-cortex. - conditions.push( - "EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ? OR value LIKE ? || '/%' ESCAPE '\\')", - ) - queryParams.push(params.tag, escapeLikeWildcards(params.tag)) + // matches both #project and #project/vault-cortex, in any letter case. + conditions.push(`EXISTS (SELECT 1 FROM json_each(t.tags) WHERE ${NESTED_TAG_PREDICATE})`) + queryParams.push(nestedTagLikePattern(params.tag)) } if (params.heading !== undefined) { @@ -1123,16 +1127,35 @@ export const listTasks = ( return { total, tasks: taskEntries } } -/** Returns all tags in the vault with their note counts. */ +/** Returns all tags in the vault with their note counts, spellings that differ + * only in letter case merged under the most-used spelling. */ export const listAllTags = ( context: SearchQueryContext, _params: Record, logger: Logger, ): TagCount[] => { + // Spellings group by their folded form. A group's count is its distinct + // notes, and the spelling shown is the one with the most occurrences across + // notes, which is Obsidian's Tags view rule, with ties broken by binary order. const sql = ` - SELECT value as tag, COUNT(DISTINCT notes.path) as count - FROM notes, json_each(notes.tags) - GROUP BY value + WITH spelling AS ( + SELECT fold_tag(value) AS folded, value AS tag, COUNT(*) AS occurrences + FROM notes, json_each(notes.tags) + GROUP BY folded, tag + ), + folded_count AS ( + SELECT fold_tag(value) AS folded, COUNT(DISTINCT notes.path) AS count + FROM notes, json_each(notes.tags) + GROUP BY folded + ), + ranked AS ( + SELECT spelling.folded, spelling.tag, + ROW_NUMBER() OVER (PARTITION BY spelling.folded ORDER BY spelling.occurrences DESC, spelling.tag) AS rank + FROM spelling + ) + SELECT ranked.tag AS tag, folded_count.count AS count + FROM ranked JOIN folded_count ON folded_count.folded = ranked.folded + WHERE ranked.rank = 1 ORDER BY count DESC, tag ` const results = context.db.prepare(sql).all() From 6bb61412fcf6e9bf9b9bad4ce274a180eeba5ada Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:20:38 -0400 Subject: [PATCH 02/12] fix(review): document the tags parser and tag listing merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit List obsidian-markdown/tags in the AGENTS.md structure tree, say in ARCHITECTURE.md that a leading # on a tag input is dropped and a lone # rejected, describe how vault_list_tags merges letter-case spellings, and name the tag comparison helpers in the search-helpers row. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- ARCHITECTURE.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6ec5fdacd..97878445e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ src/ __tests__/docker/ # :remote image boot tests (npm run test:remote-boot): docker-harness, and fixtures/ob, a stub of the obsidian-headless CLI vault-mcp/ server.ts, config.ts # Entry point and env-var loader, the only loose files here - obsidian-markdown/ # Pure parsers and transforms with no I/O (pdf-engine excepted): lines, frontmatter, callouts, headings, links, tasks, recurrence, memory-entries, canvas, plaintext, pdf + pdf-engine, moment-format + obsidian-markdown/ # Pure parsers and transforms with no I/O (pdf-engine excepted): lines, frontmatter, callouts, headings, links, tags, tasks, recurrence, memory-entries, canvas, plaintext, pdf + pdf-engine, moment-format vault-operations/ # Vault read, write, and patch: vault-filesystem (base I/O), vault-patcher, note-mover, memory-store, daily-notes, vault-folder-config, task-mutations, task-format-config, trash-config, trash-sweeper, asset-operations mcp-core/ # MCP protocol surface: mcp-router, tool-registry, tool-availability, tool-definitions, prompt-definitions tools/ # One -tools module per data-layer domain (vault-crud, search, task, memory, daily-note, asset), plus tool-helpers diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dadd21a6b..456528db9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -221,7 +221,9 @@ Both `vault_delete_note` and `vault_move_note` support `prune_empty_folders` to `vault_search_by_tag` matches a frontmatter tag and every tag nested under it (`project` matches `project/a/b` but not `projects`), or the tag alone with `exact: true`. It returns the most recently modified notes first, up to `limit` (default 20). -Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) matches nested tags the same way and ignores letter case. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. +Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) matches nested tags the same way and ignores letter case. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. A leading `#` on an input is dropped, and an input that is only `#` is rejected. + +`vault_list_tags` merges spellings that differ only in letter case into one entry, shown under the most-used spelling and counting each note once. `vault_recent_notes` sorts by `sort_by` — `"created"` or `"modified"` (default `"modified"`). @@ -579,7 +581,7 @@ The search query and indexing layer is split into seven modules (the embedding p | `search-index.ts` | Factory/closure (`createSearchIndex`), schema, migrations, write operations (`upsertNote`, `removeNote`, `rebuildFromVault`, `upsertFileContent`, `removeFileContent`), embedder + reranker wiring | | `search-queries.ts` | `SearchQueryContext` + 16 query methods — `fullTextSearch`, `memoryRecall`, `searchByTag`, `searchByFolder`, `listTasks`, `recentNotes`, `listAllTags`, `listPropertyKeys`, `listPropertyValues`, `searchByProperty`, `getBacklinks`, `getOutgoingLinks`, `findOrphans`, `brokenLinkCount`, `modifiedOnDate`, `vaultStats` | | `hybrid-search.ts` | `hybridSearch` — runs the note FTS, note vector, file-content FTS, and file-content vector legs, fuses them with RRF, applies cross-encoder reranking; falls back to FTS-only when no embeddings are available | -| `search-helpers.ts` | Pure data transforms — row mappers (`rowToMetadata`, `rowToTaskEntry`, `noteRowToSearchResult`, `fileContentRowToSearchResult`), filters (`noteMatchesSearchFilters`), snippet construction | +| `search-helpers.ts` | Pure data transforms — row mappers (`rowToMetadata`, `rowToTaskEntry`, `noteRowToSearchResult`, `fileContentRowToSearchResult`), filters (`noteMatchesSearchFilters`), tag comparison (`foldTag`, `NESTED_TAG_PREDICATE`), snippet construction | | `fts-query.ts` | FTS5 query sanitization — compound-term handling, reserved-word stripping, phrase extraction | | `rrf.ts` | Reciprocal Rank Fusion scoring (`computeRrfScores`) — rank accumulation, k=60, top-rank bonuses | | `reranker.ts` | Cross-encoder factory (`createReranker`, ms-marco-MiniLM-L-6-v2) + position-aware score blending (`blendScores`, `normalizeScores`) | From dad61b9dda803a190201de66f983e9a7557f18c9 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:32:51 -0400 Subject: [PATCH 03/12] style: clarify tag comparison comments, docs, and tag parameter texts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comments state why the tags parser keeps repeats, what an empty tag input means, and how the tag listing picks a spelling. searchByTag picks its predicate and bound value in one branch. The tag tool texts say a leading # is optional, show a nested match, and say tags come back in the note's letter case; the tool-surface snapshots are regenerated. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 7 +++-- .../__snapshots__/tool-surface/default.json | 12 +++---- .../tool-surface/disabled-tools.json | 12 +++---- .../tool-surface/embedding-off.json | 12 +++---- .../file-tools-off+embedding-off.json | 12 +++---- .../tool-surface/file-tools-off.json | 12 +++---- .../memory-off+embedding-off.json | 12 +++---- ...mory-off+file-tools-off+embedding-off.json | 12 +++---- .../memory-off+file-tools-off.json | 12 +++---- .../tool-surface/memory-off.json | 12 +++---- .../tool-surface/obsidian-sync.json | 12 +++---- .../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 +++---- src/vault-mcp/mcp-core/tools/search-tools.ts | 14 ++++----- src/vault-mcp/mcp-core/tools/task-tools.ts | 2 +- src/vault-mcp/mcp-core/tools/tool-helpers.ts | 7 +++-- src/vault-mcp/obsidian-markdown/tags.ts | 13 +++++--- src/vault-mcp/search/search-helpers.ts | 31 +++++++++++-------- src/vault-mcp/search/search-index.ts | 3 ++ src/vault-mcp/search/search-queries.ts | 31 ++++++++++--------- 26 files changed, 170 insertions(+), 154 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 456528db9..e1de5c6fb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -223,7 +223,7 @@ Both `vault_delete_note` and `vault_move_note` support `prune_empty_folders` to Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) matches nested tags the same way and ignores letter case. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. A leading `#` on an input is dropped, and an input that is only `#` is rejected. -`vault_list_tags` merges spellings that differ only in letter case into one entry, shown under the most-used spelling and counting each note once. +`vault_list_tags` merges spellings that differ only in letter case into one entry. The entry shows the spelling with the most occurrences, where a repeat within one note counts again, and its count is the number of notes carrying any of the spellings. `vault_recent_notes` sorts by `sort_by` — `"created"` or `"modified"` (default `"modified"`). @@ -236,9 +236,10 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag | `vault_list_property_values` | `key, folder?, limit?` | readOnlyHint | | `vault_search_by_property` | `key, value, folder?, limit?` | readOnlyHint | -**Promoted properties:** Five frontmatter keys — `title`, `tags`, `type`, `created`, `related` — get dedicated columns in the `notes` table for direct `WHERE`-clause filtering (no `json_extract` needed). In tool responses, these appear as top-level fields; remaining frontmatter keys are returned under `additional_properties` (via `formatNoteMetadata` in `tool-helpers.ts`). All other properties live in a JSON `properties` column — functional for any schema, but without dedicated columns. The property queries match a key as data through `json_each` over that column, never as a JSON path, so a property named `a.b` or `k[0]` is matched like any other. Array values are unpacked via `json_each`, so scalar and list properties both match. +**Promoted properties:** Five frontmatter keys — `title`, `tags` (in any letter case), `type`, `created`, `related` — get dedicated columns in the `notes` table for direct `WHERE`-clause filtering (no `json_extract` needed). In tool responses, these appear as top-level fields; remaining frontmatter keys are returned under `additional_properties` (via `formatNoteMetadata` in `tool-helpers.ts`). -- **Tags column:** holds the tags Obsidian's Tags view shows for the note, read by `obsidian-markdown/tags.ts` from the first key spelled `tags` in any letter case. A text value is one tag and a list keeps its text entries; each loses a leading `#` and a trailing `/`, and only names Obsidian accepts as tags are kept (no spaces, none of the punctuation Obsidian rejects such as `,`, `:` or `+`, and not digits only). +- **Tags column:** holds the tags Obsidian's Tags view shows for the note, read by `obsidian-markdown/tags.ts` from the first key spelled `tags` in any letter case. A text value is one tag, so `tags: a, b` gives no tags, because a tag name cannot contain `,`. A list keeps its text entries. Each entry loses a leading `#` and a trailing `/`, and only names Obsidian accepts as tags are kept (no spaces, none of the punctuation Obsidian rejects such as `,`, `:` or `+`, and not digits only). +- **Other properties:** every unpromoted key lives in a JSON `properties` column — functional for any schema, but without dedicated columns. The property queries match a key as data through `json_each` over that column, never as a JSON path, so a property named `a.b` or `k[0]` is matched like any other. Array values are unpacked via `json_each`, so scalar and list properties both match. - **Listing values:** `vault_list_property_values`, and the `sample_values` in `vault_list_property_keys`, count each value by the text the tool returns. The number `1` and the text `"1"` both return `"1"`, so they share one count; the text `"1.0"` keeps its own. Counting happens before `limit` and sample selection apply, and each item of a list property counts on its own. - **Matching a value:** `vault_search_by_property` matches text exactly. A value written as a whole, finite number in [YAML 1.2 core schema](https://yaml.org/spec/1.2.2/#1032-tag-resolution) form (`4`, `-0.5`, `1e3`, `0x1F`, `0o17`) also matches stored numbers equal to it as [JavaScript numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_encoding). So `"04"` matches the number `4` and the text `"04"`, but not the text `"4"`. Checkbox values match only `"1"` and `"0"`. 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 3afc7c5fd..93f24d302 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 @@ -631,7 +631,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -834,7 +834,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1283,7 +1283,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1302,7 +1302,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1512,14 +1512,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 9d0e6fd29..4a4d5fac4 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1225,7 +1225,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1244,7 +1244,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1454,14 +1454,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 528e5db2d..250907027 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1304,7 +1304,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1514,14 +1514,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 95a2175b8..2eaa2d5fe 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 @@ -593,7 +593,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -796,7 +796,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1201,7 +1201,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1220,7 +1220,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1430,14 +1430,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 44a3af854..6043c02c6 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 @@ -592,7 +592,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -795,7 +795,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1200,7 +1200,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1219,7 +1219,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1429,14 +1429,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 03f8c07f5..ff831ccbb 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 @@ -534,7 +534,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -737,7 +737,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1146,7 +1146,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1165,7 +1165,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1375,14 +1375,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 e48478478..36ca103e0 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 @@ -494,7 +494,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -697,7 +697,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1062,7 +1062,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1081,7 +1081,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1291,14 +1291,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 c3f24a0cc..82f205725 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 @@ -493,7 +493,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -696,7 +696,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1061,7 +1061,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1080,7 +1080,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1290,14 +1290,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 be93fcd97..fcaa74f57 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 @@ -533,7 +533,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -736,7 +736,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1145,7 +1145,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1164,7 +1164,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1374,14 +1374,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 9c14b27a1..5f9bf3fff 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -836,7 +836,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1304,7 +1304,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1514,14 +1514,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, 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 61e8b00d7..a9d502ba0 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 @@ -315,7 +315,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -518,7 +518,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -774,7 +774,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -793,7 +793,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1003,14 +1003,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json index ec6d0a060..904b0c39e 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 @@ -275,7 +275,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -478,7 +478,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -690,7 +690,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -709,7 +709,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -919,14 +919,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json index c4f1280d3..b78f3f0e2 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 @@ -274,7 +274,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -477,7 +477,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -689,7 +689,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -708,7 +708,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -918,14 +918,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json index 4ffdef4f9..ce1db1c8f 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 @@ -262,7 +262,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -465,7 +465,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -681,7 +681,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -700,7 +700,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -910,14 +910,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json index fdbdb5c80..b1525a72f 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 @@ -222,7 +222,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -425,7 +425,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -597,7 +597,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -616,7 +616,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -826,14 +826,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json index 9e20ef529..49abc4e08 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 @@ -221,7 +221,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -424,7 +424,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -596,7 +596,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -615,7 +615,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -825,14 +825,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json index 6f923ba6c..6bda02813 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 @@ -261,7 +261,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -464,7 +464,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -680,7 +680,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -699,7 +699,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -909,14 +909,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json index a17ec7c1f..d9754b5df 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 @@ -314,7 +314,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -517,7 +517,7 @@ "minLength": 1 }, "tag": { - "description": "Inline task tag; parent tags match children, letter case ignored", + "description": "Tag on the task line, \"#\" optional; matches nested tags, any letter case", "type": "string", "minLength": 1 }, @@ -773,7 +773,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -792,7 +792,7 @@ "minLength": 1 }, "tags": { - "description": "Tags to match (e.g. [\"reference\"])", + "description": "Tags, \"#\" optional (e.g. [\"reference\"])", "type": "array", "items": { "type": "string", @@ -1002,14 +1002,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" does not match notes tagged \"project\"; drop the trailing \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag name (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 0371be810..2711e7e94 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -26,7 +26,7 @@ export const registerSearchTools = ({ Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case -- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case +- tags: frontmatter tags, all required, any letter case; "project" also matches "project/a" - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -49,7 +49,7 @@ Returns: JSON with results array (path, title, snippet, score, tags, folder, typ Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case -- tags: every listed tag must match, as itself or as a parent of a nested tag, ignoring letter case +- tags: frontmatter tags, all required, any letter case; "project" also matches "project/a" - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -76,7 +76,7 @@ Returns: JSON with results array (path, title, snippet, score, tags, folder, typ tags: z .array(z.string().min(1)) .optional() - .describe('Tags to match (e.g. ["reference"])'), + .describe('Tags, "#" optional (e.g. ["reference"])'), related: z .array(z.string().min(1)) .optional() @@ -171,16 +171,16 @@ Behavior: Reads the search index, which picks up a file change within a few seco Errors: - An unknown tag or no matches returns an empty array, not an error. -- "project/" does not match notes tagged "project"; drop the trailing "/". +- "project/" matches neither "project" nor "project/a"; drop the "/". - "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name. -Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag, not only the matched one), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, +Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { tag: z .string() .min(1) .describe( - 'Tag name (e.g. "project", "session-log"). Hierarchical tags use "/" separators (e.g. "project/vault-cortex").', + 'Tag, "#" optional (e.g. "project", "session-log"). Nested tags use "/" (e.g. "project/vault-cortex").', ), exact: z .boolean() @@ -217,7 +217,7 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce TOOL_NAMES.VAULT_LIST_TAGS, { title: "List Tags", - description: `List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags in note bodies. Spellings that differ only in letter case are one tag, shown as its most-used spelling. Each hierarchical tag (e.g. "project/vault-cortex") appears as one full entry, not split into segments. Count is unique notes, not occurrences. + description: `List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. "project/vault-cortex") appears as one full entry, not split into segments. Count is unique notes, not occurrences. Example: vault_list_tags() returns [{ tag: "session-log", count: 42 }, { tag: "project/vault-cortex", count: 8 }, ...] diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index 835738852..42b89ca48 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -82,7 +82,7 @@ Returns: JSON { total, tasks }. Every task carries path, line, status, status_ch .string() .min(1) .optional() - .describe("Inline task tag; parent tags match children, letter case ignored"), + .describe('Tag on the task line, "#" optional; matches nested tags, any letter case'), heading: z .union([z.string().min(1), z.array(z.string().min(1)).min(1)]) .optional() diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index 8be384c9d..bdf90b0a0 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -42,8 +42,11 @@ export type ToolRegistrationContext = ToolAvailability & // so the response doesn't contain the same data twice. const PROMOTED_KEYS = new Set(["title", "tags", "type", "created", "related"]) -// The index reads the tags from the first key spelled `tags` in any letter -// case, as Obsidian does, so a `Tags:` key is promoted too +// Every key spelled `tags` in any letter case counts as promoted: +// - the index reads the note's tags from the first such key, as Obsidian does +// (TAGS_KEY_RE in obsidian-markdown/tags.ts), and returns them as `tags`; +// - a second such key, which the index ignores, stays out of +// additional_properties too, so the tags are reported once, under `tags`. const isPromotedKey = (key: string): boolean => { return PROMOTED_KEYS.has(key) || key.toLowerCase() === "tags" } diff --git a/src/vault-mcp/obsidian-markdown/tags.ts b/src/vault-mcp/obsidian-markdown/tags.ts index 5e171f2f2..1e72f9116 100644 --- a/src/vault-mcp/obsidian-markdown/tags.ts +++ b/src/vault-mcp/obsidian-markdown/tags.ts @@ -21,15 +21,17 @@ const TAGS_KEY_RE = /^tags$/i export const isObsidianTagName = (name: string): boolean => TAG_NAME_RE.test(name) && !DIGITS_ONLY_RE.test(name) -/** The raw `tags` value under the first key spelled `tags` in any letter case, - * which is the key Obsidian reads, even when its value is empty. */ +/** The raw value under the first key spelled `tags` in any letter case, the + * only key Obsidian reads: a later `Tags` key is ignored even if the first + * one is empty. */ const findTagsValue = (frontmatter: Record): unknown => { const tagsKey = Object.keys(frontmatter).find((key) => TAGS_KEY_RE.test(key)) return tagsKey ? frontmatter[tagsKey] : undefined } /** The text entries of a `tags` value, each trimmed: a text value is one - * entry, a list keeps its text elements, and any other value has none. */ + * entry, a list keeps its text elements, and any other value has none. An + * entry that trims to "" stays here and fails `isObsidianTagName` later. */ const textEntriesOf = (value: unknown): string[] => { if (typeof value === "string") return [value.trim()] if (Array.isArray(value)) { @@ -47,8 +49,9 @@ const tagNameOf = (entry: string): string => { /** * The tags Obsidian's Tags view shows for a note's properties, as bare names - * in document order, one per occurrence. Reproduces Obsidian 1.14.4's - * `parseFrontMatterTags` followed by `getTags`'s name test: + * in document order. A repeated tag is kept once per occurrence, because + * `listAllTags` picks the spelling it shows by occurrence count. Reproduces + * Obsidian 1.14.4's `parseFrontMatterTags` followed by `getTags`'s name test: * - the first key spelled `tags` in any letter case is read; * - a text value is one entry and a list keeps its text elements, trimmed; * - one leading `#` and one trailing `/` are removed; diff --git a/src/vault-mcp/search/search-helpers.ts b/src/vault-mcp/search/search-helpers.ts index a838b375b..5e5a00230 100644 --- a/src/vault-mcp/search/search-helpers.ts +++ b/src/vault-mcp/search/search-helpers.ts @@ -32,7 +32,7 @@ export const coerceToArray = (value: unknown): string[] => { if (Array.isArray(value)) return value.filter((element) => element != null && typeof element !== "object").map(String) - // A mapping value (tags: { project: true }) is dropped, as the array branch + // A mapping value (related: { note: true }) is dropped, as the array branch // above drops mappings in a list; String() would index it as "[object Object]". if (typeof value === "object") return [] return value ? [String(value)] : [] @@ -41,35 +41,39 @@ export const coerceToArray = (value: unknown): string[] => { // ── Tag comparison ───────────────────────────────────────────── /** Folds a tag for comparison with `toLowerCase()`, the rule Obsidian's Tags - * view groups spellings by. Shared by the `fold_tag` SQL function and the - * JavaScript mirror of the SQL tag filters, so both compare by one rule. */ + * view groups spellings by. Shared by the `fold_tag` SQL function (registered + * in search-index.ts) and the JavaScript mirror of the SQL tag filters, so + * both compare by one rule. */ export const foldTag = (tag: string): string => tag.toLowerCase() /** * Folds a tag input the way the index compares it: one leading `#` removed, - * then `foldTag`. Throws when nothing is left, because an empty prefix would - * match every nested tag. + * then `foldTag`. Throws when nothing is left, because an empty name is not a + * tag and a clear error beats a silently empty result. Tool schemas reject an + * empty string first (`.min(1)`), so the input that reaches the throw is "#". */ export const normalizeTagQuery = (input: string): string => { - const name = input.startsWith("#") ? input.slice(1) : input + const tagName = input.startsWith("#") ? input.slice(1) : input - if (name === "") throw new Error('tag must not be empty after its leading "#"') - return foldTag(name) + if (tagName === "") throw new Error('tag must not be empty after its leading "#"') + return foldTag(tagName) } /** True when a stored tag is the folded query or nested under it. The * JavaScript side of NESTED_TAG_PREDICATE. */ export const tagMatchesQuery = (params: { storedTag: string; foldedQuery: string }): boolean => { - const folded = foldTag(params.storedTag) - return folded === params.foldedQuery || folded.startsWith(`${params.foldedQuery}/`) + const foldedStoredTag = foldTag(params.storedTag) + return ( + foldedStoredTag === params.foldedQuery || foldedStoredTag.startsWith(`${params.foldedQuery}/`) + ) } /** * SQL predicate over a `json_each` `value` that matches the query tag itself * or any tag nested under it, folding each stored value once; bound to * `nestedTagLikePattern(input)`. Appending `/` to the stored value turns - * "equal or nested" into one LIKE, so `project/%` matches `project` and - * `project/x` but not `projects`. + * "equal or nested" into one LIKE (`||` binds tighter than LIKE), so + * `project/%` matches `project` and `project/x` but not `projects`. */ export const NESTED_TAG_PREDICATE = "fold_tag(value) || '/' LIKE ? ESCAPE '\\'" @@ -350,7 +354,8 @@ const propertyValueMatches = (params: { } /** Mirrors fullTextSearch's SQL filters for vector-only results. Date filter - * values are pre-validated by fullTextSearch, which hybridSearch runs first. */ + * values and tag inputs are pre-validated by fullTextSearch, which + * hybridSearch runs first, so their throws never fire here. */ export const noteMatchesSearchFilters = (note: NoteRow, filters: SearchFilters): boolean => { if (filters.folder && !pathIsInFolder({ path: note.path, folder: filters.folder })) return false diff --git a/src/vault-mcp/search/search-index.ts b/src/vault-mcp/search/search-index.ts index ff2023f43..f4b65145b 100644 --- a/src/vault-mcp/search/search-index.ts +++ b/src/vault-mcp/search/search-index.ts @@ -1463,8 +1463,11 @@ export const createSearchIndex = ( unreadableBlockError, } = parseNoteForIndex(rawContent) + // Only tags follow Obsidian's Tags view rules (any letter case for the + // key, the tag-name test); related keeps every scalar value const tags = parseFrontmatterTags(frontmatter) const related = coerceToArray(frontmatter.related) + const bodyLines = splitIntoLines(noteBody) // Store the leading callout (a top-of-file `> [!type]` block — info, diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index 7b011951d..b0f583439 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -768,7 +768,8 @@ export const memoryRecall = async ( // ── Discovery queries ────────────────────────────────────────── -/** Finds notes with a specific tag, ignoring letter case. Supports hierarchical prefix matching. */ +/** Finds notes with a tag, ignoring letter case. Unless `exact` is set, a tag + * nested under it matches too (`project` matches `project/a`, not `projects`). */ export const searchByTag = ( context: SearchQueryContext, params: { @@ -780,23 +781,19 @@ export const searchByTag = ( ): NoteMetadata[] => { const limit = Math.max(0, Math.floor(params.limit ?? 20)) - const condition = params.exact - ? "EXISTS (SELECT 1 FROM json_each(n.tags) WHERE fold_tag(value) = ?)" - : `EXISTS (SELECT 1 FROM json_each(n.tags) WHERE ${NESTED_TAG_PREDICATE})` - - const queryParams: unknown[] = params.exact - ? [normalizeTagQuery(params.tag), limit] - : [nestedTagLikePattern(params.tag), limit] + const tagMatch = params.exact + ? { predicate: "fold_tag(value) = ?", boundValue: normalizeTagQuery(params.tag) } + : { predicate: NESTED_TAG_PREDICATE, boundValue: nestedTagLikePattern(params.tag) } const sql = ` SELECT path, title, tags, related, folder, type, created, mtime, properties, leading_callout, bytes FROM notes n - WHERE ${condition} + WHERE EXISTS (SELECT 1 FROM json_each(n.tags) WHERE ${tagMatch.predicate}) ORDER BY mtime DESC, path LIMIT ? ` - const rows = context.db.prepare(sql).all(...queryParams) + const rows = context.db.prepare(sql).all(tagMatch.boundValue, limit) const results = rows.map(rowToMetadata) logger.info("search by tag", { tag: params.tag, @@ -1040,8 +1037,9 @@ export const listTasks = ( } if (params.tag !== undefined) { - // Same nested-tag semantics as searchByTag's prefix mode: "project" - // matches both #project and #project/vault-cortex, in any letter case. + // Same nested-tag semantics as searchByTag without `exact`: "project" + // matches the stored task tags project and project/vault-cortex (kept + // without "#"), in any letter case. conditions.push(`EXISTS (SELECT 1 FROM json_each(t.tags) WHERE ${NESTED_TAG_PREDICATE})`) queryParams.push(nestedTagLikePattern(params.tag)) } @@ -1134,9 +1132,12 @@ export const listAllTags = ( _params: Record, logger: Logger, ): TagCount[] => { - // Spellings group by their folded form. A group's count is its distinct - // notes, and the spelling shown is the one with the most occurrences across - // notes, which is Obsidian's Tags view rule, with ties broken by binary order. + // Spellings group by their folded form: + // - a group's count is its distinct notes; + // - the spelling shown is the one with the most occurrences, a repeat within + // one note counting again, which is Obsidian's Tags view rule; + // - a tie goes to the spelling first in SQLite's default BINARY collation + // (byte order). const sql = ` WITH spelling AS ( SELECT fold_tag(value) AS folded, value AS tag, COUNT(*) AS occurrences From 51e1342796c1a7bb027c29fa3133a31445816fcf Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:34:28 -0400 Subject: [PATCH 04/12] refactor(search): share the tags-key predicate between the parser and the response formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tags.ts exports isTagsKey, the one rule for a property key spelled tags in any letter case, and tool-helpers.ts uses it for the promoted-key strip instead of restating the rule with toLowerCase(). Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- src/vault-mcp/mcp-core/tools/tool-helpers.ts | 7 ++++--- .../obsidian-markdown/__tests__/tags.test.ts | 12 +++++++++++- src/vault-mcp/obsidian-markdown/tags.ts | 5 ++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index bdf90b0a0..2b4170d3f 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -11,6 +11,7 @@ import type { LineWindow } from "../../obsidian-markdown/lines.js" import type { ToolName } from "../tool-registry.js" import type { ToolAvailability } from "../tool-availability.js" import { describeError } from "../../../utils/describe-error.js" +import { isTagsKey } from "../../obsidian-markdown/tags.js" import { OverwriteBlockedError, UnkeepableOpeningBlockError, @@ -43,12 +44,12 @@ export type ToolRegistrationContext = ToolAvailability & const PROMOTED_KEYS = new Set(["title", "tags", "type", "created", "related"]) // Every key spelled `tags` in any letter case counts as promoted: -// - the index reads the note's tags from the first such key, as Obsidian does -// (TAGS_KEY_RE in obsidian-markdown/tags.ts), and returns them as `tags`; +// - the index reads the note's tags from the first such key, as Obsidian does, +// and returns them as `tags`; // - a second such key, which the index ignores, stays out of // additional_properties too, so the tags are reported once, under `tags`. const isPromotedKey = (key: string): boolean => { - return PROMOTED_KEYS.has(key) || key.toLowerCase() === "tags" + return PROMOTED_KEYS.has(key) || isTagsKey(key) } /** Reshapes NoteMetadata for client responses: keeps all top-level fields, diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts index 6ab4525b9..83168c381 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts @@ -1,5 +1,15 @@ import { describe, it, expect } from "vitest" -import { isObsidianTagName, parseFrontmatterTags } from "../tags.js" +import { isObsidianTagName, isTagsKey, parseFrontmatterTags } from "../tags.js" + +describe("isTagsKey", () => { + it.each(["tags", "Tags", "TAGS"])("accepts %s", (key) => { + expect(isTagsKey(key)).toBe(true) + }) + + it.each(["tag", "tags ", "#tags", "related"])("rejects %j", (key) => { + expect(isTagsKey(key)).toBe(false) + }) +}) describe("isObsidianTagName", () => { it.each([ diff --git a/src/vault-mcp/obsidian-markdown/tags.ts b/src/vault-mcp/obsidian-markdown/tags.ts index 1e72f9116..ae1e4d2ef 100644 --- a/src/vault-mcp/obsidian-markdown/tags.ts +++ b/src/vault-mcp/obsidian-markdown/tags.ts @@ -17,6 +17,9 @@ const DIGITS_ONLY_RE = /^\d+$/ /** Matches the `tags` property key in any letter case, as Obsidian looks it up. */ const TAGS_KEY_RE = /^tags$/i +/** True for a property key Obsidian reads tags from: `tags` in any letter case. */ +export const isTagsKey = (key: string): boolean => TAGS_KEY_RE.test(key) + /** True for a name that Obsidian's Tags view would list as a tag. */ export const isObsidianTagName = (name: string): boolean => TAG_NAME_RE.test(name) && !DIGITS_ONLY_RE.test(name) @@ -25,7 +28,7 @@ export const isObsidianTagName = (name: string): boolean => * only key Obsidian reads: a later `Tags` key is ignored even if the first * one is empty. */ const findTagsValue = (frontmatter: Record): unknown => { - const tagsKey = Object.keys(frontmatter).find((key) => TAGS_KEY_RE.test(key)) + const tagsKey = Object.keys(frontmatter).find(isTagsKey) return tagsKey ? frontmatter[tagsKey] : undefined } From 35914fda9685b40b0f35da02f70ccf18ff685959 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:40:33 -0400 Subject: [PATCH 05/12] test(search): make the tag tests fail for the rule they name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - listAllTags: the merge fixture now separates ranking by occurrences from ranking by notes and from the binary tie-break, and both merge tests assert the whole tag list - searchByTag: the Projects and my-project decoys sit in their own note, so a match on either one shows up - new tests: a stored non-ASCII tag in prefix mode, a leading # in exact mode, the enriched chunk prefix's tags, a trimmed text tags value, and a Supplemental Punctuation name - exact error and fold assertions; it.each labels within vitest's 40-character title limit Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/tags.test.ts | 11 +- .../search/__tests__/search-helpers.test.ts | 16 ++- .../search/__tests__/search-index.test.ts | 112 +++++++++++++----- .../search/__tests__/task-queries.test.ts | 2 +- 4 files changed, 104 insertions(+), 37 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts index 83168c381..576805fef 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tags.test.ts @@ -29,9 +29,12 @@ describe("isObsidianTagName", () => { { label: "a comma", name: "alpha,beta" }, { label: "a plus sign", name: "c++" }, { label: "a colon", name: "a:b" }, - { label: "a hash inside the name", name: "#x" }, + { label: "a hash sign", name: "#x" }, + { label: "a square bracket", name: "a]b" }, { label: "digits only", name: "1984" }, - { label: "a zero-width joiner, which sits in the General Punctuation block", name: "a‍b" }, + // Labels stay within 40 characters, past which vitest cuts the title + { label: "a General Punctuation zero-width joiner", name: "a‍b" }, + { label: "a Supplemental Punctuation character", name: "a⸮b" }, ])("rejects $label", ({ name }) => { expect(isObsidianTagName(name)).toBe(false) }) @@ -42,6 +45,10 @@ describe("parseFrontmatterTags", () => { expect(parseFrontmatterTags({ tags: "my-tag" })).toEqual(["my-tag"]) }) + it("trims a text value and removes its leading #", () => { + expect(parseFrontmatterTags({ tags: " #my-tag " })).toEqual(["my-tag"]) + }) + it("reads no tag from a comma-joined text value", () => { expect(parseFrontmatterTags({ tags: "alpha, beta" })).toEqual([]) }) diff --git a/src/vault-mcp/search/__tests__/search-helpers.test.ts b/src/vault-mcp/search/__tests__/search-helpers.test.ts index ab5187ff1..e72f44d35 100644 --- a/src/vault-mcp/search/__tests__/search-helpers.test.ts +++ b/src/vault-mcp/search/__tests__/search-helpers.test.ts @@ -510,11 +510,13 @@ describe("foldTag", () => { }) it("folds a capital sharp s to the small one, so the pair compares equal", () => { - expect(foldTag("ẞ")).toBe(foldTag("ß")) + expect(foldTag("ẞ")).toBe("ß") + expect(foldTag("ß")).toBe("ß") }) it("keeps sharp s apart from double s, which Obsidian lists apart", () => { - expect(foldTag("Straße")).not.toBe(foldTag("STRASSE")) + expect(foldTag("Straße")).toBe("straße") + expect(foldTag("STRASSE")).toBe("strasse") }) }) @@ -529,7 +531,9 @@ describe("normalizeTagQuery", () => { }) it("throws when nothing is left after the leading #", () => { - expect(() => normalizeTagQuery("#")).toThrow('tag must not be empty after its leading "#"') + expect(() => normalizeTagQuery("#")).toThrow( + new Error('tag must not be empty after its leading "#"'), + ) }) }) @@ -573,7 +577,9 @@ describe("nestedTagLikePattern", () => { }) it("throws for an input that is only a #", () => { - expect(() => nestedTagLikePattern("#")).toThrow('tag must not be empty after its leading "#"') + expect(() => nestedTagLikePattern("#")).toThrow( + new Error('tag must not be empty after its leading "#"'), + ) }) }) @@ -626,7 +632,7 @@ describe("noteMatchesSearchFilters", () => { it("rejects a tag filter that is only a #", () => { expect(() => noteMatchesSearchFilters(baseRow, { tags: ["#"] })).toThrow( - 'tag must not be empty after its leading "#"', + new Error('tag must not be empty after its leading "#"'), ) }) diff --git a/src/vault-mcp/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index 5521ef32e..78a2555d5 100644 --- a/src/vault-mcp/search/__tests__/search-index.test.ts +++ b/src/vault-mcp/search/__tests__/search-index.test.ts @@ -1254,18 +1254,11 @@ describe("upsertNote", () => { }) it.each([ - { - label: 'a quoted "#Alpha" entry stored without the #', - frontmatter: 'tags: ["#Alpha"]', - stored: ["Alpha"], - }, - { - label: "a comma-joined text value stored as no tag", - frontmatter: "tags: alpha, beta", - stored: [], - }, - { label: "a numeric text value stored as no tag", frontmatter: "tags: 2024", stored: [] }, - { label: "a Tags key read in its own letter case", frontmatter: "Tags: [x]", stored: ["x"] }, + // Labels stay within 40 characters, past which vitest cuts the title + { label: 'a quoted "#Alpha" as Alpha', frontmatter: 'tags: ["#Alpha"]', stored: ["Alpha"] }, + { label: "a comma-joined text value as no tag", frontmatter: "tags: alpha, beta", stored: [] }, + { label: "a bare number as no tag", frontmatter: "tags: 2024", stored: [] }, + { label: "the tags under a Tags key", frontmatter: "Tags: [x]", stored: ["x"] }, ])("stores $label", ({ frontmatter, stored }) => { index.upsertNote( { @@ -1714,7 +1707,7 @@ describe("fullTextSearch", () => { return index.fullTextSearch({ query: "notes", filters: { tags: ["#"] } }, logger) } - expect(searchWithEmptyTag).toThrow('tag must not be empty after its leading "#"') + expect(searchWithEmptyTag).toThrow(new Error('tag must not be empty after its leading "#"')) }) it("respects type filter", () => { @@ -2083,11 +2076,21 @@ describe("searchByTag", () => { index.upsertNote( { filePath: "e.md", - rawContent: "---\ntags: [project, Projects, my-project]\n---\nbody\n", + rawContent: "---\ntags: [project]\n---\nbody\n", fileStat: testStat(5000), }, logger, ) + // The decoys sit in a note of their own, so a match on either one would + // add f.md to the results + index.upsertNote( + { + filePath: "f.md", + rawContent: "---\ntags: [Projects, my-project]\n---\nbody\n", + fileStat: testStat(6000), + }, + logger, + ) expect(index.searchByTag({ tag: "PROJECT" }, logger).map((result) => result.path)).toEqual([ "e.md", @@ -2114,9 +2117,29 @@ describe("searchByTag", () => { expect(index.searchByTag({ tag: "STRASSE" }, logger)).toEqual([]) }) + it("folds a stored non-ASCII tag in prefix mode, where LIKE alone ignores only ASCII case", () => { + index.upsertNote( + { + filePath: "d.md", + rawContent: "---\ntags: [Ärger/Alt]\n---\nbody\n", + fileStat: testStat(4000), + }, + logger, + ) + + expect(index.searchByTag({ tag: "ärger" }, logger).map((result) => result.path)).toEqual([ + "d.md", + ]) + }) + + it('removes a leading "#" in exact mode too', () => { + const results = index.searchByTag({ tag: "#Project/Vault-MCP", exact: true }, logger) + expect(results.map((result) => result.path)).toEqual(["a.md"]) + }) + it("rejects a tag that is only a #", () => { expect(() => index.searchByTag({ tag: "#" }, logger)).toThrow( - 'tag must not be empty after its leading "#"', + new Error('tag must not be empty after its leading "#"'), ) }) @@ -2319,7 +2342,7 @@ describe("listAllTags", () => { index.upsertNote( { filePath: "b.md", - rawContent: "---\ntags: [Work]\n---\nbody\n", + rawContent: "---\ntags: [work]\n---\nbody\n", fileStat: testStat(3000), }, logger, @@ -2327,18 +2350,27 @@ describe("listAllTags", () => { index.upsertNote( { filePath: "c.md", - rawContent: "---\ntags: [WORK, WORK, WORK]\n---\nbody\n", + rawContent: "---\ntags: [Work, Work, Work]\n---\nbody\n", fileStat: testStat(4000), }, logger, ) + index.upsertNote( + { + filePath: "d.md", + rawContent: "---\ntags: [WORK]\n---\nbody\n", + fileStat: testStat(5000), + }, + logger, + ) - // `WORK` has three occurrences in one note, `work` and `Work` one each: - // the spelling follows occurrences, the count follows distinct notes - const workEntry = index - .listAllTags({}, logger) - .find((entry) => entry.tag.toLowerCase() === "work") - expect(workEntry).toEqual({ tag: "WORK", count: 3 }) + // `Work` wins with three occurrences in one note. Ranking by notes would + // pick `work` (two notes), and the binary tie-break alone would pick `WORK` + expect(index.listAllTags({}, logger)).toEqual([ + { tag: "Work", count: 4 }, + { tag: "principles", count: 2 }, + { tag: "self", count: 1 }, + ]) }) it("breaks a spelling tie by binary order and counts a note with both spellings once", () => { @@ -2351,12 +2383,14 @@ describe("listAllTags", () => { logger, ) - // One occurrence each: `Tie` sorts before `tie` in binary order, and the - // single note carrying both counts once - const tieEntry = index - .listAllTags({}, logger) - .find((entry) => entry.tag.toLowerCase() === "tie") - expect(tieEntry).toEqual({ tag: "Tie", count: 1 }) + // One occurrence each: `Tie` sorts before `tie` in binary order, though + // `tie` comes first in the note, and the single note carrying both counts once + expect(index.listAllTags({}, logger)).toEqual([ + { tag: "principles", count: 2 }, + { tag: "Tie", count: 1 }, + { tag: "self", count: 1 }, + { tag: "work", count: 1 }, + ]) }) it("adds no tag for a mapping-valued tags property", () => { @@ -6135,6 +6169,26 @@ It has multiple sentences to verify chunking works correctly. ) }) + it("prefixes the tags the index stores: a Tags key read, # removed, invalid names left out", async () => { + const mockEmbedder = createMockEmbedder() + const enrichedIndex = createSearchIndex(":memory:", mockEmbedder, undefined, { + ranking: { enrichChunkMetadata: true }, + }) + const rawContent = + '---\ntitle: Tagged Note\nTags: ["#Search", "a b", "1984"]\n---\n\nBody content.\n' + + const sourceVersion = seedEmbeddingSource(enrichedIndex, { + notePath: "tagged.md", + rawContent, + }) + await enrichedIndex.embedNote({ sourceVersion, notePath: "tagged.md", rawContent }, logger) + + expect(mockEmbedder.embedText).toHaveBeenCalledTimes(1) + expect(mockEmbedder.embedText).toHaveBeenCalledWith( + "Tagged Note\nTags: Search.\n\n\nBody content.", + ) + }) + it("embeds unprefixed chunk text under enrichment when the note has no type or tags", async () => { const mockEmbedder = createMockEmbedder() const enrichedIndex = createSearchIndex(":memory:", mockEmbedder, undefined, { diff --git a/src/vault-mcp/search/__tests__/task-queries.test.ts b/src/vault-mcp/search/__tests__/task-queries.test.ts index 558f484bc..91997ff3f 100644 --- a/src/vault-mcp/search/__tests__/task-queries.test.ts +++ b/src/vault-mcp/search/__tests__/task-queries.test.ts @@ -620,7 +620,7 @@ describe("listTasks scope filters", () => { const index = indexWithBoardAndPlain() expect(() => index.listTasks({ tag: "#" }, logger)).toThrow( - 'tag must not be empty after its leading "#"', + new Error('tag must not be empty after its leading "#"'), ) }) From c9095892475fc547f85f832f3af89c651b3bb4a3 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:49:53 -0400 Subject: [PATCH 06/12] fix(search): keep a second tags-spelled key in additional_properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit formatNoteMetadata strips only the tags key the index read (the first key spelled tags in any letter case), so a later key such as TAGS with its own values stays in additional_properties instead of vanishing from the response. Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- ARCHITECTURE.md | 2 +- .../tools/__tests__/tool-helpers.test.ts | 10 ++++++--- src/vault-mcp/mcp-core/tools/tool-helpers.ts | 22 +++++++++---------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e1de5c6fb..e69873f0e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -236,7 +236,7 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag | `vault_list_property_values` | `key, folder?, limit?` | readOnlyHint | | `vault_search_by_property` | `key, value, folder?, limit?` | readOnlyHint | -**Promoted properties:** Five frontmatter keys — `title`, `tags` (in any letter case), `type`, `created`, `related` — get dedicated columns in the `notes` table for direct `WHERE`-clause filtering (no `json_extract` needed). In tool responses, these appear as top-level fields; remaining frontmatter keys are returned under `additional_properties` (via `formatNoteMetadata` in `tool-helpers.ts`). +**Promoted properties:** Five frontmatter keys — `title`, `tags` (the first key so spelled in any letter case), `type`, `created`, `related` — get dedicated columns in the `notes` table for direct `WHERE`-clause filtering (no `json_extract` needed). In tool responses, these appear as top-level fields; remaining frontmatter keys are returned under `additional_properties` (via `formatNoteMetadata` in `tool-helpers.ts`). - **Tags column:** holds the tags Obsidian's Tags view shows for the note, read by `obsidian-markdown/tags.ts` from the first key spelled `tags` in any letter case. A text value is one tag, so `tags: a, b` gives no tags, because a tag name cannot contain `,`. A list keeps its text entries. Each entry loses a leading `#` and a trailing `/`, and only names Obsidian accepts as tags are kept (no spaces, none of the punctuation Obsidian rejects such as `,`, `:` or `+`, and not digits only). - **Other properties:** every unpromoted key lives in a JSON `properties` column — functional for any schema, but without dedicated columns. The property queries match a key as data through `json_each` over that column, never as a JSON path, so a property named `a.b` or `k[0]` is matched like any other. Array values are unpacked via `json_each`, so scalar and list properties both match. diff --git a/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts b/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts index ae8980c50..42e6dc0cf 100644 --- a/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts +++ b/src/vault-mcp/mcp-core/tools/__tests__/tool-helpers.test.ts @@ -433,14 +433,18 @@ describe("formatNoteMetadata", () => { }) }) - it("treats a tags key in any letter case as promoted, so a Tags: note reports its tags once", () => { + it("drops the tags key the index read, in any letter case, and keeps a later one", () => { const formatted = formatNoteMetadata({ path: "a.md", tags: ["x"], leading_callout: null, - properties: { Tags: ["x"], TAGS: ["y"] }, + properties: { Tags: ["x"], tags: ["y"] }, }) - expect(formatted).toEqual({ path: "a.md", tags: ["x"] }) + expect(formatted).toEqual({ + path: "a.md", + tags: ["x"], + additional_properties: { tags: ["y"] }, + }) }) }) diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index 2b4170d3f..3c3ef272b 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -40,16 +40,15 @@ export type ToolRegistrationContext = ToolAvailability & // Frontmatter keys that are already top-level fields on NoteMetadata. // These are stripped from `properties` before returning to clients -// so the response doesn't contain the same data twice. -const PROMOTED_KEYS = new Set(["title", "tags", "type", "created", "related"]) - -// Every key spelled `tags` in any letter case counts as promoted: -// - the index reads the note's tags from the first such key, as Obsidian does, -// and returns them as `tags`; -// - a second such key, which the index ignores, stays out of -// additional_properties too, so the tags are reported once, under `tags`. -const isPromotedKey = (key: string): boolean => { - return PROMOTED_KEYS.has(key) || isTagsKey(key) +// so the response doesn't contain the same data twice. The tags key is +// found per note: the index reads the first key spelled `tags` in any +// letter case, as Obsidian does, and returns it as `tags`; a second such +// key, which the index ignores, stays in additional_properties. +const PROMOTED_KEYS = new Set(["title", "type", "created", "related"]) + +const promotedKeysOf = (properties: Record): ReadonlySet => { + const tagsKeyRead = Object.keys(properties).find(isTagsKey) + return tagsKeyRead ? new Set([...PROMOTED_KEYS, tagsKeyRead]) : PROMOTED_KEYS } /** Reshapes NoteMetadata for client responses: keeps all top-level fields, @@ -63,8 +62,9 @@ export const formatNoteMetadata = (meta: { // keep it (the { type, title, body } block) when present. const { properties, leading_callout: leadingCallout, ...fields } = meta + const promotedKeys = promotedKeysOf(properties) const additional_properties = Object.fromEntries( - Object.entries(properties).filter(([key]) => !isPromotedKey(key)), + Object.entries(properties).filter(([key]) => !promotedKeys.has(key)), ) return { From 4072d650c24c7327b40eecabc9910552def897ef Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:07:21 -0400 Subject: [PATCH 07/12] docs: list tags.ts in the wiki's parser inventories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- .devin/wiki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devin/wiki.json b/.devin/wiki.json index 95975131b..e89a5f4a2 100644 --- a/.devin/wiki.json +++ b/.devin/wiki.json @@ -4,7 +4,7 @@ "content": "Vault Cortex is a remote MCP (Model Context Protocol) server that exposes an Obsidian vault over HTTPS — vault CRUD, hybrid search, task management, structured memory, link-graph queries, and non-markdown file reading (images, PDFs, canvases, data files), plus user-initiated prompts. It runs headless in Docker, working directly with .md files on disk using its own Obsidian-parity parsers and search index. README.md is the authoritative feature description and ARCHITECTURE.md the authoritative design document — derive capability details, defaults, and security specifics from those and the code rather than restating them. The repo also ships a CLI (cli/, published as the vault-cortex npm package) that scaffolds a deployment: npx vault-cortex@latest init." }, { - "content": "The codebase is organized into four internal layers under src/vault-mcp/: (1) obsidian-markdown/ — pure Obsidian/Markdown parsers with no I/O (links, lines/fences, frontmatter, callouts, headings, tasks, memory entries, canvas, plaintext). (2) vault-operations/ — filesystem I/O for vault content (vault-filesystem, vault-patcher, note-mover, memory-store, daily-notes, vault-folder-config, task-mutations, task-format-config, trash-sweeper, asset-operations). (3) search/ — the SQLite FTS5 + sqlite-vec index, embedding pipeline, and chokidar file watcher. (4) mcp-core/ — the MCP protocol surface: streamable-http transport, tool group modules under mcp-core/tools/, and prompt group modules under mcp-core/prompts/. Authentication lives in src/vault-mcp/oauth/ (OAuth 2.1) and src/functions/authorizer.ts (AWS Lambda, path-aware). The :remote image's setup mode — the browser sign-in served before an Obsidian Sync token exists — lives in src/vault-mcp/setup/. Infrastructure-as-code (sst.config.ts) is specific to the AWS reference deployment and should not be documented as the primary deployment path." + "content": "The codebase is organized into four internal layers under src/vault-mcp/: (1) obsidian-markdown/ — pure Obsidian/Markdown parsers with no I/O (links, lines/fences, frontmatter, tags, callouts, headings, tasks, memory entries, canvas, plaintext, PDF). (2) vault-operations/ — filesystem I/O for vault content (vault-filesystem, vault-patcher, note-mover, memory-store, daily-notes, vault-folder-config, task-mutations, task-format-config, trash-sweeper, asset-operations). (3) search/ — the SQLite FTS5 + sqlite-vec index, embedding pipeline, and chokidar file watcher. (4) mcp-core/ — the MCP protocol surface: streamable-http transport, tool group modules under mcp-core/tools/, and prompt group modules under mcp-core/prompts/. Authentication lives in src/vault-mcp/oauth/ (OAuth 2.1) and src/functions/authorizer.ts (AWS Lambda, path-aware). The :remote image's setup mode — the browser sign-in served before an Obsidian Sync token exists — lives in src/vault-mcp/setup/. Infrastructure-as-code (sst.config.ts) is specific to the AWS reference deployment and should not be documented as the primary deployment path." } ], "pages": [ @@ -40,7 +40,7 @@ }, { "title": "Obsidian Markdown Parsing", - "purpose": "The pure parser layer in obsidian-markdown/ — no filesystem I/O: links.ts, lines.ts, frontmatter.ts, callouts.ts, headings.ts, tasks.ts, recurrence.ts, memory-entries.ts, canvas.ts, plaintext.ts, moment-format.ts. This layer encodes the Obsidian-parity behavior (every link form and 'New link format' mode Obsidian recognizes; both Tasks-plugin formats) that distinguishes Vault Cortex from simpler file-based integrations.", + "purpose": "The pure parser layer in obsidian-markdown/ — no filesystem I/O: links.ts, lines.ts, frontmatter.ts, tags.ts, callouts.ts, headings.ts, tasks.ts, recurrence.ts, memory-entries.ts, canvas.ts, plaintext.ts, pdf.ts, pdf-engine.ts, moment-format.ts. This layer encodes the Obsidian-parity behavior (every link form and 'New link format' mode Obsidian recognizes; both Tasks-plugin formats) that distinguishes Vault Cortex from simpler file-based integrations.", "parent": "Architecture" }, { From bd39ecc16eb9d98c30d5b3425cfcbe51fd7f0a86 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:36:45 -0400 Subject: [PATCH 08/12] docs: state the exact-mode exception, the accepted-tags rule, and the per-note tags key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCHITECTURE.md scopes the nested-tag sentence to non-exact searches; vault_search_by_tag's Returns says the tags array holds the frontmatter tags Obsidian accepts, as written; vault_search_by_property's Returns names the tags key the index read among the keys kept out of additional_properties. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- ARCHITECTURE.md | 2 +- .../__tests__/__snapshots__/tool-surface/default.json | 4 ++-- .../__snapshots__/tool-surface/disabled-tools.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/embedding-off.json | 4 ++-- .../tool-surface/file-tools-off+embedding-off.json | 4 ++-- .../__snapshots__/tool-surface/file-tools-off.json | 4 ++-- .../tool-surface/memory-off+embedding-off.json | 4 ++-- .../memory-off+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/memory-off+file-tools-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/obsidian-sync.json | 4 ++-- .../__snapshots__/tool-surface/readonly+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off+embedding-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+memory-off+file-tools-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly+memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/readonly.json | 4 ++-- src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts | 3 +-- src/vault-mcp/mcp-core/tools/search-tools.ts | 6 +++--- 21 files changed, 41 insertions(+), 42 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e69873f0e..2928b7e39 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -221,7 +221,7 @@ Both `vault_delete_note` and `vault_move_note` support `prune_empty_folders` to `vault_search_by_tag` matches a frontmatter tag and every tag nested under it (`project` matches `project/a/b` but not `projects`), or the tag alone with `exact: true`. It returns the most recently modified notes first, up to `limit` (default 20). -Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) matches nested tags the same way and ignores letter case. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. A leading `#` on an input is dropped, and an input that is only `#` is rejected. +Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tags` filter on `vault_search`) ignores letter case and, unless `exact: true` is set on `vault_search_by_tag`, matches nested tags the same way. Both sides of a comparison are folded with `toLowerCase()`, the rule Obsidian's Tags view groups spellings by. A leading `#` on an input is dropped, and an input that is only `#` is rejected. `vault_list_tags` merges spellings that differ only in letter case into one entry. The entry shows the spelling with the most occurrences, where a repeat within one note counts again, and its count is the number of notes carrying any of the spellings. 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 93f24d302..8cc237c56 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 @@ -1466,7 +1466,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1512,7 +1512,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 4a4d5fac4..c5286962c 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 @@ -1408,7 +1408,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1454,7 +1454,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 250907027..83540ad19 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 @@ -1468,7 +1468,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 2eaa2d5fe..dfa7868d9 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1430,7 +1430,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 6043c02c6..4a0265337 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 @@ -1383,7 +1383,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1429,7 +1429,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 ff831ccbb..25db1f679 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1375,7 +1375,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 36ca103e0..db02f400b 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1291,7 +1291,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 82f205725..fc009408f 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 @@ -1244,7 +1244,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1290,7 +1290,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 fcaa74f57..55df9ee7c 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 @@ -1328,7 +1328,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1374,7 +1374,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 5f9bf3fff..7d0873633 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 @@ -1468,7 +1468,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 a9d502ba0..4f286a628 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1003,7 +1003,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 904b0c39e..dad784f2e 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -919,7 +919,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 b78f3f0e2..78f64b80b 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -918,7 +918,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 ce1db1c8f..bdef08be8 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -910,7 +910,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 b1525a72f..00e7ec5f7 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -826,7 +826,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 49abc4e08..475b4a3c3 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -825,7 +825,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 6bda02813..e64073c67 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -909,7 +909,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 d9754b5df..ac821049a 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 @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", "inputSchema": { "type": "object", "properties": { @@ -1002,7 +1002,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you also need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 0839d6fa9..a5fe043c5 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -2741,8 +2741,7 @@ describe("DISABLED_TOOLS", () => { const TAG_SEARCH_SCOPE_SENTENCE = "When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query." const TAG_SEARCH_LIST_TAGS_SENTENCE = " Use vault_list_tags first to discover available tags." - const TAG_SEARCH_SEARCH_LINE = - "\nPrefer vault_search when you also need text-based relevance ranking." + const TAG_SEARCH_SEARCH_LINE = "\nPrefer vault_search when you need text-based relevance ranking." it.each([ { diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 2711e7e94..ebb606c95 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -162,7 +162,7 @@ Returns: JSON with results array (path, title, snippet, score, tags, folder, typ Example: vault_search_by_tag({ tag: "project" }) Example: vault_search_by_tag({ tag: "project", exact: true, limit: 50 }) — notes tagged "project" itself, up to 50 -When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query.${whenToolEnabledText("vault_list_tags", " Use vault_list_tags first to discover available tags.")}${whenToolEnabledText("vault_search", "\nPrefer vault_search when you also need text-based relevance ranking.")} +When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query.${whenToolEnabledText("vault_list_tags", " Use vault_list_tags first to discover available tags.")}${whenToolEnabledText("vault_search", "\nPrefer vault_search when you need text-based relevance ranking.")} Parameters: - limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check. @@ -174,7 +174,7 @@ Errors: - "project/" matches neither "project" nor "project/a"; drop the "/". - "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name. -Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (all the note's frontmatter tags, in their own letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, +Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { tag: z .string() @@ -481,7 +481,7 @@ Errors: Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits. - leading_callout appears only when the note has a leading callout. -- additional_properties appears only when frontmatter has keys outside title, tags, type, created, and related.`, +- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.`, inputSchema: { key: z.string().min(1).describe('Property key name (e.g. "status", "type", "tags").'), value: z.string().min(1).describe('Value to match (e.g. "active", "4", "1e-7").'), From aca31ffb50b15fa13366fca5ddcf5f8f7a8486bd Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:09:22 -0400 Subject: [PATCH 09/12] docs: say every listed tag must match, and quote the empty-tag error once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vault_search tags bullet read "all required"; it now says every listed frontmatter tag must match. The empty-tag Errors bullets in the three tools no longer wrap a message that contains quotes in a second pair of quotes. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- .../__tests__/__snapshots__/tool-surface/default.json | 6 +++--- .../__snapshots__/tool-surface/disabled-tools.json | 6 +++--- .../__snapshots__/tool-surface/embedding-off.json | 6 +++--- .../tool-surface/file-tools-off+embedding-off.json | 6 +++--- .../__snapshots__/tool-surface/file-tools-off.json | 6 +++--- .../tool-surface/memory-off+embedding-off.json | 6 +++--- .../memory-off+file-tools-off+embedding-off.json | 6 +++--- .../tool-surface/memory-off+file-tools-off.json | 6 +++--- .../__snapshots__/tool-surface/memory-off.json | 6 +++--- .../__snapshots__/tool-surface/obsidian-sync.json | 6 +++--- .../tool-surface/readonly+embedding-off.json | 6 +++--- .../readonly+file-tools-off+embedding-off.json | 6 +++--- .../tool-surface/readonly+file-tools-off.json | 6 +++--- .../readonly+memory-off+embedding-off.json | 6 +++--- ...adonly+memory-off+file-tools-off+embedding-off.json | 6 +++--- .../readonly+memory-off+file-tools-off.json | 6 +++--- .../tool-surface/readonly+memory-off.json | 6 +++--- .../__tests__/__snapshots__/tool-surface/readonly.json | 6 +++--- src/vault-mcp/mcp-core/tools/search-tools.ts | 10 +++++----- src/vault-mcp/mcp-core/tools/task-tools.ts | 2 +- 20 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 8cc237c56..7e25d2038 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 @@ -650,7 +650,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1283,7 +1283,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1512,7 +1512,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 c5286962c..ee59dca0f 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 @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1225,7 +1225,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1454,7 +1454,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 83540ad19..adbc3ce8c 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 @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 dfa7868d9..08e341ba6 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 @@ -612,7 +612,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1201,7 +1201,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1430,7 +1430,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 4a0265337..c53ee8fca 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 @@ -611,7 +611,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1200,7 +1200,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1429,7 +1429,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 25db1f679..8360d3459 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 @@ -553,7 +553,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1146,7 +1146,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1375,7 +1375,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 db02f400b..5b6c73fd1 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 @@ -513,7 +513,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1062,7 +1062,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1291,7 +1291,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 fc009408f..d2745a03e 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 @@ -512,7 +512,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1061,7 +1061,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1290,7 +1290,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 55df9ee7c..845a65eb8 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 @@ -552,7 +552,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1145,7 +1145,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1374,7 +1374,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 7d0873633..fa6eda72a 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 @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1285,7 +1285,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 4f286a628..811332258 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 @@ -334,7 +334,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1003,7 +1003,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 dad784f2e..46b652205 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 @@ -294,7 +294,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -690,7 +690,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -919,7 +919,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 78f64b80b..e200a9c31 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 @@ -293,7 +293,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -689,7 +689,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -918,7 +918,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 bdef08be8..0a613a829 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 @@ -281,7 +281,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -681,7 +681,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -910,7 +910,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 00e7ec5f7..44877c55e 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 @@ -241,7 +241,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -597,7 +597,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -826,7 +826,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 475b4a3c3..fc429b2de 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 @@ -240,7 +240,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -596,7 +596,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -825,7 +825,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 e64073c67..b15e96e9f 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 @@ -280,7 +280,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -680,7 +680,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -909,7 +909,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { 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 ac821049a..59558b790 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 @@ -333,7 +333,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: frontmatter tags, all required, any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"tag must not be empty after its leading \"#\"\" — a tags entry is \"#\" alone; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" }). Values compare by exact type — pass a number as a number, not \"4\". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- tag must not be empty after its leading \"#\" — a tags entry is only \"#\"; pass the tag name\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1002,7 +1002,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty after its leading \"#\"\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index ebb606c95..d853688ac 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -26,7 +26,7 @@ export const registerSearchTools = ({ Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case -- tags: frontmatter tags, all required, any letter case; "project" also matches "project/a" +- tags: every listed frontmatter tag must match, in any letter case; "project" also matches "project/a" - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -42,14 +42,14 @@ Errors: - No matches returns { results: [], total: 0 }, not an error - Malformed query syntax is sanitized automatically — the tool never throws a query syntax error - A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") -- "tag must not be empty after its leading "#"" — a tags entry is "#" alone; pass the tag name +- tag must not be empty after its leading "#" — a tags entry is only "#"; pass the tag name Returns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode ("hybrid" or "fts"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — "hybrid" when vector embeddings contributed, "fts" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is "note" for markdown notes or "file" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".canvas", ".pdf", ".txt"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.` : `Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the "narrow by metadata, search by text" pattern. Unquoted terms use implicit AND with porter stemming; wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically. Filters — all conditions AND-combine with each other and the text query: - folder: a whole folder, subfolders included — "Projects" covers "Projects/Archive" but not "ProjectsOld/"; ignores ASCII letter case -- tags: frontmatter tags, all required, any letter case; "project" also matches "project/a" +- tags: every listed frontmatter tag must match, in any letter case; "project" also matches "project/a" - properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: "active" }). Values compare by exact type — pass a number as a number, not "4". Exception: checkbox values are stored as 1 and 0, so pass true to match 1 and false to match 0. A list property matches when any element equals the value. - created / modified: bounds compare whole calendar days, server-local, so on matches anywhere within the day. Notes without a parseable created property never match a created filter @@ -65,7 +65,7 @@ Errors: - No matches returns { results: [], total: 0 }, not an error - Malformed query syntax is sanitized automatically — the tool never throws a query syntax error - A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") -- "tag must not be empty after its leading "#"" — a tags entry is "#" alone; pass the tag name +- tag must not be empty after its leading "#" — a tags entry is only "#"; pass the tag name Returns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode ("fts" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is "note" for markdown notes or "file" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".canvas", ".pdf", ".txt"). created is omitted when null. bytes is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.`, inputSchema: { @@ -172,7 +172,7 @@ Behavior: Reads the search index, which picks up a file change within a few seco Errors: - An unknown tag or no matches returns an empty array, not an error. - "project/" matches neither "project" nor "project/a"; drop the "/". -- "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name. +- tag must not be empty after its leading "#" — tag is "#" alone; pass the tag name. Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index 42b89ca48..67354ce4d 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -45,7 +45,7 @@ Parameters: Errors: - A malformed or calendar-invalid date filter throws with remediation text ("Use YYYY-MM-DD") - path without the ".md" extension is rejected -- "tag must not be empty after its leading "#"" — tag is "#" alone; pass the tag name +- tag must not be empty after its leading "#" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error Returns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status "done" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.`, From af1205a1100e1349a24b1a335062646ce574b2d7 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:17:25 -0400 Subject: [PATCH 10/12] docs: apply the tool-definition review of the tag tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vault_search_by_tag's Returns names the tags array as every frontmatter tag Obsidian accepts, in the note's letter case; vault_list_tags says only tags Obsidian accepts are counted and names the secondary sort; the vault_search_by_property bullet names the first key spelled tags in any letter case instead of "the tags key read"; vault_list_tasks's path error keeps its remedy. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- .../__tests__/__snapshots__/tool-surface/default.json | 8 ++++---- .../__snapshots__/tool-surface/disabled-tools.json | 8 ++++---- .../__snapshots__/tool-surface/embedding-off.json | 8 ++++---- .../tool-surface/file-tools-off+embedding-off.json | 8 ++++---- .../__snapshots__/tool-surface/file-tools-off.json | 8 ++++---- .../tool-surface/memory-off+embedding-off.json | 8 ++++---- .../memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/memory-off+file-tools-off.json | 8 ++++---- .../__tests__/__snapshots__/tool-surface/memory-off.json | 8 ++++---- .../__snapshots__/tool-surface/obsidian-sync.json | 8 ++++---- .../tool-surface/readonly+embedding-off.json | 8 ++++---- .../readonly+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/readonly+file-tools-off.json | 8 ++++---- .../tool-surface/readonly+memory-off+embedding-off.json | 8 ++++---- .../readonly+memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/readonly+memory-off+file-tools-off.json | 8 ++++---- .../__snapshots__/tool-surface/readonly+memory-off.json | 8 ++++---- .../__tests__/__snapshots__/tool-surface/readonly.json | 8 ++++---- src/vault-mcp/mcp-core/tools/search-tools.ts | 8 ++++---- src/vault-mcp/mcp-core/tools/task-tools.ts | 2 +- 20 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 7e25d2038..0d0a8e74c 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 @@ -631,7 +631,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -650,7 +650,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1466,7 +1466,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1512,7 +1512,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 ee59dca0f..fd7aad709 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1408,7 +1408,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1454,7 +1454,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 adbc3ce8c..dcd5aea60 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1468,7 +1468,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 08e341ba6..4e1ccd4c7 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 @@ -593,7 +593,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -612,7 +612,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1430,7 +1430,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 c53ee8fca..5c80dcbb1 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 @@ -592,7 +592,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -611,7 +611,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1383,7 +1383,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1429,7 +1429,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 8360d3459..a82325bc9 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 @@ -534,7 +534,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -553,7 +553,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1375,7 +1375,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 5b6c73fd1..f6e22f5ab 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 @@ -494,7 +494,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -513,7 +513,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1291,7 +1291,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 d2745a03e..02e08dd0f 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 @@ -493,7 +493,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -512,7 +512,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1244,7 +1244,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1290,7 +1290,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 845a65eb8..a1f2c2e51 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 @@ -533,7 +533,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -552,7 +552,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1328,7 +1328,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1374,7 +1374,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 fa6eda72a..bf47bbb6f 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 @@ -633,7 +633,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -652,7 +652,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -1468,7 +1468,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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1514,7 +1514,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 811332258..fe714cdb4 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 @@ -315,7 +315,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -334,7 +334,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1003,7 +1003,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 46b652205..98246ac3d 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 @@ -275,7 +275,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -294,7 +294,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -919,7 +919,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 e200a9c31..d892adf63 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 @@ -274,7 +274,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -293,7 +293,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -918,7 +918,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 0a613a829..4d8ab402b 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 @@ -262,7 +262,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -281,7 +281,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -910,7 +910,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 44877c55e..1d8dacbba 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 @@ -222,7 +222,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -241,7 +241,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -826,7 +826,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 fc429b2de..1ae2d50fe 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 @@ -221,7 +221,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -240,7 +240,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -825,7 +825,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 b15e96e9f..4abe2fff2 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 @@ -261,7 +261,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -280,7 +280,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -909,7 +909,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "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 59558b790..117ef35f3 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 @@ -314,7 +314,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -333,7 +333,7 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a task written moments ago may still show its old state.\n\nParameters:\n- status: virtual values expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { @@ -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.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.", + "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).\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -1002,7 +1002,7 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Reads the search index, which picks up a file change within a few seconds, so a note written moments ago may not appear yet.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- tag must not be empty after its leading \"#\" — tag is \"#\" alone; pass the tag name.\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index d853688ac..526db335c 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -174,7 +174,7 @@ Errors: - "project/" matches neither "project" nor "project/a"; drop the "/". - tag must not be empty after its leading "#" — tag is "#" alone; pass the tag name. -Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (the note's frontmatter tags Obsidian accepts, as written), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, +Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { tag: z .string() @@ -217,7 +217,7 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce TOOL_NAMES.VAULT_LIST_TAGS, { title: "List Tags", - description: `List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag (e.g. "project/vault-cortex") appears as one full entry, not split into segments. Count is unique notes, not occurrences. + description: `List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences. Example: vault_list_tags() returns [{ tag: "session-log", count: 42 }, { tag: "project/vault-cortex", count: 8 }, ...] @@ -460,7 +460,7 @@ Returns: JSON array of { value, count } sorted by count descending, then by valu TOOL_NAMES.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. + 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). Example: vault_search_by_property({ key: "status", value: "in-progress" }) Example: vault_search_by_property({ key: "type", value: "session-log", folder: "Code Projects" }) @@ -481,7 +481,7 @@ Errors: Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits. - leading_callout appears only when the note has a leading callout. -- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the tags key read.`, +- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.`, inputSchema: { key: z.string().min(1).describe('Property key name (e.g. "status", "type", "tags").'), value: z.string().min(1).describe('Value to match (e.g. "active", "4", "1e-7").'), diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index 67354ce4d..3a7a21d35 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -44,7 +44,7 @@ Parameters: Errors: - A malformed or calendar-invalid date filter throws with remediation text ("Use YYYY-MM-DD") -- path without the ".md" extension is rejected +- "path must end in .md" — add the ".md" extension - tag must not be empty after its leading "#" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error From ef8e07bd500e8af26955647cfbb3fb9c3a1ed81d Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:15:33 -0400 Subject: [PATCH 11/12] docs: say what a tag group's count counts Co-Authored-By: Claude Fable 5.1 --- src/vault-mcp/search/search-queries.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index b0f583439..8bb1bc6b9 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -1132,8 +1132,9 @@ export const listAllTags = ( _params: Record, logger: Logger, ): TagCount[] => { - // Spellings group by their folded form: - // - a group's count is its distinct notes; + // Spellings that fold to the same lowercase tag form one group: + // - the group's count is the number of distinct notes carrying any of its + // spellings, so a note tagged both `Project` and `project` counts once; // - the spelling shown is the one with the most occurrences, a repeat within // one note counting again, which is Obsidian's Tags view rule; // - a tie goes to the spelling first in SQLite's default BINARY collation From e71536deb91eeb3e6bf18942e17660667e689ddb Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:42:55 -0400 Subject: [PATCH 12/12] refactor: name the per-note promoted-key set for what it holds FIXED_PROMOTED_KEYS holds the four keys with one spelling; promotedKeysOfNote adds the tags key the note's frontmatter spells, and the comment says why that key varies per note. Co-Authored-By: Claude Fable 5.1 --- src/vault-mcp/mcp-core/tools/tool-helpers.ts | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index 3c3ef272b..3002fd6a1 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -38,17 +38,20 @@ export type ToolRegistrationContext = ToolAvailability & config: VaultConfig } -// Frontmatter keys that are already top-level fields on NoteMetadata. -// These are stripped from `properties` before returning to clients -// so the response doesn't contain the same data twice. The tags key is -// found per note: the index reads the first key spelled `tags` in any -// letter case, as Obsidian does, and returns it as `tags`; a second such -// key, which the index ignores, stays in additional_properties. -const PROMOTED_KEYS = new Set(["title", "type", "created", "related"]) - -const promotedKeysOf = (properties: Record): ReadonlySet => { - const tagsKeyRead = Object.keys(properties).find(isTagsKey) - return tagsKeyRead ? new Set([...PROMOTED_KEYS, tagsKeyRead]) : PROMOTED_KEYS +// Five frontmatter keys are promoted to top-level fields on NoteMetadata and +// so are stripped from additional_properties, which would otherwise repeat +// them. Four have one fixed spelling. The fifth, `tags`, is spelled per note: +// the index reads the first key spelled `tags` in any letter case (`tags`, +// `Tags`, `TAGS`), as Obsidian does, so that key is the one stripped, and a +// later tags-spelled key, which the index ignores, stays in +// additional_properties. +const FIXED_PROMOTED_KEYS = new Set(["title", "type", "created", "related"]) + +/** The promoted keys of one note: the fixed four plus the tags key its + * frontmatter spells, when it has one. */ +const promotedKeysOfNote = (properties: Record): ReadonlySet => { + const noteTagsKey = Object.keys(properties).find(isTagsKey) + return noteTagsKey ? new Set([...FIXED_PROMOTED_KEYS, noteTagsKey]) : FIXED_PROMOTED_KEYS } /** Reshapes NoteMetadata for client responses: keeps all top-level fields, @@ -62,7 +65,7 @@ export const formatNoteMetadata = (meta: { // keep it (the { type, title, body } block) when present. const { properties, leading_callout: leadingCallout, ...fields } = meta - const promotedKeys = promotedKeysOf(properties) + const promotedKeys = promotedKeysOfNote(properties) const additional_properties = Object.fromEntries( Object.entries(properties).filter(([key]) => !promotedKeys.has(key)), )