From a563eee9d689e170a548b2d83df357f8624bbdf7 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:17:57 -0400 Subject: [PATCH 1/5] fix: say when a date filter or task date names a day the calendar lacks A date in YYYY-MM-DD form that names a missing day, such as 2026-02-30, was answered with "Use YYYY-MM-DD", the form the caller had already used. The vault_search created/modified filters, the vault_list_tasks date filters, and the vault_create_task / vault_update_task date params now say the date is not a calendar date; text not in that form keeps the format message. classifyIsoDate in src/utils/ makes the split for all of them. Co-Authored-By: Claude Opus 5.5 --- .../server-error-contracts.test.ts | 91 ++++++++++++++++++- src/utils/__tests__/classify-iso-date.test.ts | 33 +++++++ src/utils/classify-iso-date.ts | 19 ++++ .../__snapshots__/tool-surface/default.json | 4 +- .../tool-surface/disabled-tools.json | 4 +- .../tool-surface/embedding-off.json | 4 +- .../file-tools-off+embedding-off.json | 4 +- .../tool-surface/file-tools-off.json | 4 +- .../memory-off+embedding-off.json | 4 +- ...mory-off+file-tools-off+embedding-off.json | 4 +- .../memory-off+file-tools-off.json | 4 +- .../tool-surface/memory-off.json | 4 +- .../tool-surface/obsidian-sync.json | 4 +- .../tool-surface/readonly+embedding-off.json | 4 +- ...readonly+file-tools-off+embedding-off.json | 4 +- .../tool-surface/readonly+file-tools-off.json | 4 +- .../readonly+memory-off+embedding-off.json | 4 +- ...mory-off+file-tools-off+embedding-off.json | 4 +- .../readonly+memory-off+file-tools-off.json | 4 +- .../tool-surface/readonly+memory-off.json | 4 +- .../__snapshots__/tool-surface/readonly.json | 4 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 6 +- src/vault-mcp/mcp-core/tools/task-tools.ts | 3 +- .../search/__tests__/search-index.test.ts | 8 +- .../search/__tests__/task-queries.test.ts | 6 +- src/vault-mcp/search/search-queries.ts | 17 ++-- .../__tests__/task-mutations.test.ts | 53 ++++++++++- .../vault-operations/task-mutations.ts | 10 +- 28 files changed, 258 insertions(+), 60 deletions(-) create mode 100644 src/utils/__tests__/classify-iso-date.test.ts create mode 100644 src/utils/classify-iso-date.ts diff --git a/src/__tests__/integration/server-error-contracts.test.ts b/src/__tests__/integration/server-error-contracts.test.ts index 1fb674a7e..3138da183 100644 --- a/src/__tests__/integration/server-error-contracts.test.ts +++ b/src/__tests__/integration/server-error-contracts.test.ts @@ -1692,7 +1692,7 @@ describe("task errors", () => { ) }) - it("vault_create_task with invalid date", async () => { + it("vault_create_task with a date the calendar lacks", async () => { const result = await callTool({ client, name: "vault_create_task", @@ -1703,10 +1703,18 @@ describe("task errors", () => { due: "2026-02-30", }, }) - expectToolError(result, "invalid date") + expect(result).toEqual({ + content: [ + { + type: "text", + text: '[Error]: invalid date: due "2026-02-30" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + ], + isError: true, + }) }) - it("vault_update_task with invalid date", async () => { + it("vault_update_task with a malformed date", async () => { const result = await callTool({ client, name: "vault_update_task", @@ -1716,7 +1724,10 @@ describe("task errors", () => { due: "not-a-date", }, }) - expectToolError(result, "invalid date") + expect(result).toEqual({ + content: [{ type: "text", text: '[Error]: invalid date: due "not-a-date" (use YYYY-MM-DD)' }], + isError: true, + }) }) it("vault_update_task — cannot move a sub-task to a heading", async () => { @@ -2535,3 +2546,75 @@ describe("tag must not be empty", () => { }) }) }) + +// ── Date filter values ─────────────────────────────────────── + +describe("date filter values", () => { + it("vault_search rejects a date filter not in YYYY-MM-DD form", async () => { + const result = await callTool({ + client, + name: "vault_search", + args: { query: "note", filters: { modified: { after: "yesterday" } } }, + }) + expect(result).toEqual({ + content: [ + { + type: "text", + text: '[Error]: invalid modified.after date: "yesterday". Use YYYY-MM-DD (e.g. 2026-07-03).', + }, + ], + isError: true, + }) + }) + + it("vault_search rejects a date filter naming a day the calendar lacks", async () => { + const result = await callTool({ + client, + name: "vault_search", + args: { query: "note", filters: { created: { on: "2026-02-30" } } }, + }) + expect(result).toEqual({ + content: [ + { + type: "text", + text: '[Error]: created.on date "2026-02-30" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + ], + isError: true, + }) + }) + + it("vault_list_tasks rejects a date filter not in YYYY-MM-DD form", async () => { + const result = await callTool({ + client, + name: "vault_list_tasks", + args: { due: { before: "July 3rd" } }, + }) + expect(result).toEqual({ + content: [ + { + type: "text", + text: '[Error]: invalid due.before date: "July 3rd". Use YYYY-MM-DD (e.g. 2026-07-03).', + }, + ], + isError: true, + }) + }) + + it("vault_list_tasks rejects a date filter naming a day the calendar lacks", async () => { + const result = await callTool({ + client, + name: "vault_list_tasks", + args: { done: { after: "2026-04-31" } }, + }) + expect(result).toEqual({ + content: [ + { + type: "text", + text: '[Error]: done.after date "2026-04-31" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + ], + isError: true, + }) + }) +}) diff --git a/src/utils/__tests__/classify-iso-date.test.ts b/src/utils/__tests__/classify-iso-date.test.ts new file mode 100644 index 000000000..216207490 --- /dev/null +++ b/src/utils/__tests__/classify-iso-date.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest" +import { classifyIsoDate } from "../classify-iso-date.js" + +describe("classifyIsoDate", () => { + it.each([ + { label: "an ordinary day", text: "2026-07-03" }, + { label: "February 29 in a leap year", text: "2024-02-29" }, + ])("classifies $label as a calendar date", ({ text }) => { + expect(classifyIsoDate(text)).toBe("calendar-date") + }) + + it.each([ + { label: "February 30", text: "2026-02-30" }, + { label: "February 29 outside a leap year", text: "2025-02-29" }, + { label: "month 13", text: "2026-13-01" }, + { label: "month 00", text: "2026-00-10" }, + { label: "day 00", text: "2026-02-00" }, + ])("classifies $label as not a calendar date", ({ text }) => { + expect(classifyIsoDate(text)).toBe("not-a-calendar-date") + }) + + it.each([ + { label: "a single-digit month and day", text: "2026-7-3" }, + { label: "a timestamp", text: "2026-07-03T10:30:00" }, + { label: "a five-digit year", text: "20260-01-01" }, + { label: "slashes", text: "2026/07/03" }, + { label: "a month name", text: "March 10" }, + { label: "surrounding whitespace", text: " 2026-07-03" }, + { label: "an empty string", text: "" }, + ])("classifies $label as malformed", ({ text }) => { + expect(classifyIsoDate(text)).toBe("malformed") + }) +}) diff --git a/src/utils/classify-iso-date.ts b/src/utils/classify-iso-date.ts new file mode 100644 index 000000000..6f723d1af --- /dev/null +++ b/src/utils/classify-iso-date.ts @@ -0,0 +1,19 @@ +import { DateTime } from "luxon" + +type IsoDateClass = "calendar-date" | "malformed" | "not-a-calendar-date" + +/** Sorts text by whether it is a real YYYY-MM-DD day. The two failures: + * - "malformed": not in that form, such as "2026-7-3", "March 10" or a timestamp + * - "not-a-calendar-date": in that form, but the day doesn't exist, such as 2026-02-30 + * They stay apart because a caller told only the format sends a missing day again. */ +export const classifyIsoDate = (text: string): IsoDateClass => { + const parsedDate = DateTime.fromFormat(text, "yyyy-MM-dd") + + if (parsedDate.isValid) return "calendar-date" + + // Luxon matches the form before it checks the calendar, and reports a month + // or day out of range as "unit out of range"; any other reason is the form + if (parsedDate.invalidReason === "unit out of range") return "not-a-calendar-date" + + return "malformed" +} 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 0d0a8e74c..5e88da5a6 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 fd7aad709..62bb8a65e 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 dcd5aea60..37483f114 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 4e1ccd4c7..9eecaa160 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 5c80dcbb1..1dcb44a24 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 a82325bc9..05e75f098 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 f6e22f5ab..2aa784b1a 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 02e08dd0f..6a8c9930e 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 a1f2c2e51..72ddaafa6 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 bf47bbb6f..3f68d1ebd 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 fe714cdb4..32ad91652 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 98246ac3d..97b66284e 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 d892adf63..732c3a85d 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 4d8ab402b..26a839037 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 1d8dacbba..38b9c5780 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 1ae2d50fe..7e53a6f12 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 4abe2fff2..86fd210c0 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { 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 117ef35f3..ede14a24c 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 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.", + "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- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- tag must not be empty 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: 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.", + "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- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- 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": { diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 526db335c..de0b8cd3b 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -41,7 +41,8 @@ Prefer vault_search_by_tag for tag-only queries without text. Prefer vault_searc 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") +- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form +- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date - 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.` @@ -64,7 +65,8 @@ Prefer vault_search_by_tag for tag-only queries without text. Prefer vault_searc 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") +- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form +- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date - 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.`, diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index 3a7a21d35..474b69f46 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -43,7 +43,8 @@ Parameters: - folder: a whole folder, subfolders included ("Projects" covers "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Errors: -- A malformed or calendar-invalid date filter throws with remediation text ("Use YYYY-MM-DD") +- "invalid due.before date: …" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form +- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date - "path must end in .md" — add the ".md" extension - tag must not be empty after its leading "#" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error diff --git a/src/vault-mcp/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index 8ef9d2db2..95526d031 100644 --- a/src/vault-mcp/search/__tests__/search-index.test.ts +++ b/src/vault-mcp/search/__tests__/search-index.test.ts @@ -6680,7 +6680,7 @@ Shared datefilter content for boundary tests. }).toThrow(/^invalid created\.on date: "March 10"\. Use YYYY-MM-DD \(e\.g\. 2026-07-03\)\.$/) }) - it("rejects a calendar-invalid created date", () => { + it("rejects a created date the calendar lacks, saying so rather than naming the format", () => { const dateIndex = indexWithCreatedDates() expect(() => { dateIndex.fullTextSearch( @@ -6691,7 +6691,7 @@ Shared datefilter content for boundary tests. logger, ) }).toThrow( - /^invalid created\.before date: "2026-02-31"\. Use YYYY-MM-DD \(e\.g\. 2026-07-03\)\.$/, + /^created\.before date "2026-02-31" is not a calendar date\. Pass a real date in YYYY-MM-DD format\.$/, ) }) }) @@ -6787,7 +6787,7 @@ Shared datefilter content for mtime boundary tests. ) }) - it("rejects a calendar-invalid modified date", () => { + it("rejects a modified date the calendar lacks, saying so rather than naming the format", () => { const dateIndex = indexWithModifiedTimes() expect(() => { dateIndex.fullTextSearch( @@ -6798,7 +6798,7 @@ Shared datefilter content for mtime boundary tests. logger, ) }).toThrow( - /^invalid modified\.before date: "2026-02-31"\. Use YYYY-MM-DD \(e\.g\. 2026-07-03\)\.$/, + /^modified\.before date "2026-02-31" is not a calendar date\. Pass a real date in YYYY-MM-DD format\.$/, ) }) diff --git a/src/vault-mcp/search/__tests__/task-queries.test.ts b/src/vault-mcp/search/__tests__/task-queries.test.ts index 91997ff3f..817cc585d 100644 --- a/src/vault-mcp/search/__tests__/task-queries.test.ts +++ b/src/vault-mcp/search/__tests__/task-queries.test.ts @@ -449,10 +449,12 @@ describe("listTasks date filters", () => { ) }) - it("rejects a calendar-invalid date", () => { + it("rejects a date the calendar lacks, saying so rather than naming the format", () => { const index = createTestIndex() expect(() => index.listTasks({ due: { on: "2026-02-31" } }, logger)).toThrow( - 'invalid due.on date: "2026-02-31". Use YYYY-MM-DD (e.g. 2026-07-03).', + new Error( + 'due.on date "2026-02-31" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + ), ) }) }) diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index 8bb1bc6b9..e87c6ec5d 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -1,8 +1,8 @@ // ── Query methods bound by createSearchIndex ───────────────── import type Database from "better-sqlite3" -import { DateTime } from "luxon" import type { Logger } from "../../logger.js" +import { classifyIsoDate } from "../../utils/classify-iso-date.js" import { describeError } from "../../utils/describe-error.js" import { compareByUtf8Bytes } from "../../utils/compare-utf8-bytes.js" import { assertPathHasExtension } from "../../utils/assert-path-has-extension.js" @@ -142,14 +142,19 @@ type FileContentMetadataRow = { // ── Full-text search ─────────────────────────────────────────── -/** Rejects a malformed date filter bound with remediation text — shared by - * the note created/modified filters and the task date filters. - * `fromFormat` with `yyyy-MM-dd` pins both the format (no time component, - * no shorthand) and calendar correctness (2026-02-31 fails) in one call. */ +/** Rejects a date filter bound that is not a real YYYY-MM-DD day, shared by + * the note created/modified filters and the task date filters. */ const assertFilterDate = (value: string, filterName: string): void => { - if (!DateTime.fromFormat(value, "yyyy-MM-dd").isValid) { + const dateClass = classifyIsoDate(value) + + if (dateClass === "malformed") { throw new Error(`invalid ${filterName} date: "${value}". Use YYYY-MM-DD (e.g. 2026-07-03).`) } + if (dateClass === "not-a-calendar-date") { + throw new Error( + `${filterName} date "${value}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, + ) + } } export const fullTextSearch = ( diff --git a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts index 1f96bed37..d96019af1 100644 --- a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts @@ -1240,7 +1240,26 @@ kanban-plugin: board ) }) - it("errors on invalid date", async () => { + it("errors on a malformed date", async () => { + const vault = await createVault() + await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) + + await expect( + taskMutations.createTask( + { + statusRegistry: DEFAULT_STATUS_REGISTRY, + vaultPath: vault, + path: "tasks.md", + description: "Bad date", + blockId: "bad-date", + due: "2026-2-3", + }, + logger, + ), + ).rejects.toThrow(new Error('invalid date: due "2026-2-3" (use YYYY-MM-DD)')) + }) + + it("errors on a date the calendar lacks, saying so rather than naming the format", async () => { const vault = await createVault() await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) @@ -1256,7 +1275,11 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow('invalid date: due "2026-02-30"') + ).rejects.toThrow( + new Error( + 'invalid date: due "2026-02-30" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + ), + ) }) it("errors when parent not found", async () => { @@ -2207,7 +2230,7 @@ kanban-plugin: board ) }) - it("errors on invalid date in update", async () => { + it("errors on a malformed date in update", async () => { const vault = await createVault() await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) @@ -2222,7 +2245,29 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow("invalid date") + ).rejects.toThrow(new Error('invalid date: due "not-a-date" (use YYYY-MM-DD)')) + }) + + it("errors on a date the calendar lacks in update, saying so rather than naming the format", async () => { + const vault = await createVault() + await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) + + await expect( + taskMutations.updateTask( + { + statusRegistry: DEFAULT_STATUS_REGISTRY, + vaultPath: vault, + path: "tasks.md", + blockId: "walk-dog", + created: "2026-13-01", + }, + logger, + ), + ).rejects.toThrow( + new Error( + 'invalid date: created "2026-13-01" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + ), + ) }) it("errors on empty description in update", async () => { diff --git a/src/vault-mcp/vault-operations/task-mutations.ts b/src/vault-mcp/vault-operations/task-mutations.ts index 48ac83602..93f8d76b8 100644 --- a/src/vault-mcp/vault-operations/task-mutations.ts +++ b/src/vault-mcp/vault-operations/task-mutations.ts @@ -6,6 +6,7 @@ import { DateTime } from "luxon" import { parseNoteForRewrite, stringifyNote } from "../obsidian-markdown/frontmatter.js" import { resolveSafePath, atomicWriteFile } from "./vault-filesystem.js" import { assertPathHasExtension } from "../../utils/assert-path-has-extension.js" +import { classifyIsoDate } from "../../utils/classify-iso-date.js" import { readFileOrNull } from "../../utils/fs.js" import { withExclusiveFileLock } from "../../utils/file-write-lock.js" import { parseHeadings, type HeadingInfo } from "../obsidian-markdown/headings.js" @@ -1406,9 +1407,16 @@ const TASK_TEXT_LINE_BREAK_PATTERN = /[\r\n]/ /** Validates a date string is a real calendar date. */ const validateDate = (date: string, fieldName: string): void => { - if (!DateTime.fromFormat(date, "yyyy-MM-dd").isValid) { + const dateClass = classifyIsoDate(date) + + if (dateClass === "malformed") { throw new Error(`invalid date: ${fieldName} "${date}" (use YYYY-MM-DD)`) } + if (dateClass === "not-a-calendar-date") { + throw new Error( + `invalid date: ${fieldName} "${date}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, + ) + } } type ParentLocator = { kind: "blockId"; blockId: string } | { kind: "line"; line: number } From 5d1bd2287abf6b70956ff3d69f678144ec3c5737 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:45:07 -0400 Subject: [PATCH 2/5] style: clarify the date-check comments, name the task date check assertTaskDate, share vault_search's Filters and Errors text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - classifyIsoDate's doc uses the verb it does, names both Luxon reasons, and says a month or day can be out of range - assertFilterDate and assertTaskDate return early on a real day, and their docs say which tool descriptions quote their messages - assertFilterDate gets its own section banner, since full-text search and task listing both call it - comments say why a modified bound never reaches dayToEpochMsRange's check, what a task date filter's column name is used for, and what null means for an update's date params - both vault_search description variants interpolate one Filters and one Errors section; the served text is unchanged Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- src/utils/classify-iso-date.ts | 12 +++--- src/vault-mcp/mcp-core/tools/search-tools.ts | 41 +++++++++---------- src/vault-mcp/search/search-queries.ts | 31 ++++++++++---- .../vault-operations/task-mutations.ts | 28 +++++++------ 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/utils/classify-iso-date.ts b/src/utils/classify-iso-date.ts index 6f723d1af..5576c79a7 100644 --- a/src/utils/classify-iso-date.ts +++ b/src/utils/classify-iso-date.ts @@ -2,17 +2,19 @@ import { DateTime } from "luxon" type IsoDateClass = "calendar-date" | "malformed" | "not-a-calendar-date" -/** Sorts text by whether it is a real YYYY-MM-DD day. The two failures: +/** Classifies text by whether it names a real day in YYYY-MM-DD form. The two failures: * - "malformed": not in that form, such as "2026-7-3", "March 10" or a timestamp - * - "not-a-calendar-date": in that form, but the day doesn't exist, such as 2026-02-30 - * They stay apart because a caller told only the format sends a missing day again. */ + * - "not-a-calendar-date": in that form, but the month or day doesn't exist, + * such as 2026-02-30 or 2026-13-01 + * They stay apart so an error message can name the failure: a client told only + * "use YYYY-MM-DD" sends 2026-02-30 again, since it already used that form. */ export const classifyIsoDate = (text: string): IsoDateClass => { const parsedDate = DateTime.fromFormat(text, "yyyy-MM-dd") if (parsedDate.isValid) return "calendar-date" - // Luxon matches the form before it checks the calendar, and reports a month - // or day out of range as "unit out of range"; any other reason is the form + // Luxon checks the form first ("unparsable" when it doesn't match), then the + // calendar ("unit out of range" for a month or day that doesn't exist) if (parsedDate.invalidReason === "unit out of range") return "not-a-calendar-date" return "malformed" diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index de0b8cd3b..5dce95936 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -17,6 +17,21 @@ export const registerSearchTools = ({ logger: sessionLogger, config, }: ToolRegistrationContext): void => { + // Filters and errors are the same with or without embeddings, so both + // vault_search description variants share these two sections + const searchFiltersSection = `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 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` + + const searchErrorsSection = `Errors: +- No matches returns { results: [], total: 0 }, not an error +- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error +- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form +- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date +- tag must not be empty after its leading "#" — a tags entry is only "#"; pass the tag name` + registerTool( TOOL_NAMES.VAULT_SEARCH, { @@ -24,11 +39,7 @@ export const registerSearchTools = ({ description: config.embeddingEnabled ? `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. -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 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 +${searchFiltersSection} Example: vault_search({ query: "kubernetes networking", filters: { tags: ["reference"] } }) Example: vault_search({ query: "meeting notes", filters: { type: "meeting", folder: "Work" } }) @@ -38,21 +49,12 @@ Example: vault_search({ query: "how the server watches for file changes" }) — When 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. Prefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing. -Errors: -- No matches returns { results: [], total: 0 }, not an error -- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error -- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form -- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date -- tag must not be empty after its leading "#" — a tags entry is only "#"; pass the tag name +${searchErrorsSection} 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 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 +${searchFiltersSection} Example: vault_search({ query: "kubernetes networking", filters: { tags: ["reference"] } }) Example: vault_search({ query: "meeting notes", filters: { type: "meeting", folder: "Work" } }) @@ -62,12 +64,7 @@ Example: vault_search({ query: "deployment", filters: { properties: { status: "a When to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Prefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing. -Errors: -- No matches returns { results: [], total: 0 }, not an error -- Malformed query syntax is sanitized automatically — the tool never throws a query syntax error -- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form -- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date -- tag must not be empty after its leading "#" — a tags entry is only "#"; pass the tag name +${searchErrorsSection} 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: { diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index e87c6ec5d..9423a409d 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -140,23 +140,27 @@ type FileContentMetadataRow = { bytes: number } -// ── Full-text search ─────────────────────────────────────────── +// ── Date filter validation ───────────────────────────────────── -/** Rejects a date filter bound that is not a real YYYY-MM-DD day, shared by - * the note created/modified filters and the task date filters. */ +/** Rejects a date filter bound that isn't in YYYY-MM-DD form or names a day + * the calendar lacks. + * - Used by the note created/modified filters and the task date filters + * - vault_search and vault_list_tasks quote both messages in their Errors sections */ const assertFilterDate = (value: string, filterName: string): void => { const dateClass = classifyIsoDate(value) + if (dateClass === "calendar-date") return + if (dateClass === "malformed") { throw new Error(`invalid ${filterName} date: "${value}". Use YYYY-MM-DD (e.g. 2026-07-03).`) } - if (dateClass === "not-a-calendar-date") { - throw new Error( - `${filterName} date "${value}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, - ) - } + throw new Error( + `${filterName} date "${value}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, + ) } +// ── Full-text search ─────────────────────────────────────────── + export const fullTextSearch = ( context: SearchQueryContext, params: { @@ -265,6 +269,8 @@ export const fullTextSearch = ( if (params.filters?.modified) { const { on, before, after } = params.filters.modified + // assertFilterDate checks each bound before dayToEpochMsRange converts it, + // so a bad bound gets assertFilterDate's message, never the converter's if (on !== undefined) { assertFilterDate(on, "modified.on") const dayRange = dayToEpochMsRange(on) @@ -1001,8 +1007,15 @@ export const listTasks = ( { column: "created", filter: params.created }, { column: "cancelled", filter: params.cancelled }, ] + + // column does two jobs: + // - names the SQL column, safe to interpolate because its type allows only + // the six names above + // - names the filter in error messages, which vault_list_tasks' Errors + // section quotes ("invalid due.before date: …") for (const { column, filter } of dateFilters) { - if (filter === undefined) continue + if (!filter) continue + if (filter.on !== undefined) { assertFilterDate(filter.on, `${column}.on`) conditions.push(`t.${column} = ?`) diff --git a/src/vault-mcp/vault-operations/task-mutations.ts b/src/vault-mcp/vault-operations/task-mutations.ts index 93f8d76b8..7157273e7 100644 --- a/src/vault-mcp/vault-operations/task-mutations.ts +++ b/src/vault-mcp/vault-operations/task-mutations.ts @@ -1405,18 +1405,20 @@ const assertRecurrenceRuleGrammar = (recurrenceText: string | null | undefined): * reads as part of the task. */ const TASK_TEXT_LINE_BREAK_PATTERN = /[\r\n]/ -/** Validates a date string is a real calendar date. */ -const validateDate = (date: string, fieldName: string): void => { +/** Rejects a task date param that isn't in YYYY-MM-DD form or names a day the + * calendar lacks. Both messages open with "invalid date", which the + * vault_create_task and vault_update_task Errors sections quote. */ +const assertTaskDate = (date: string, fieldName: string): void => { const dateClass = classifyIsoDate(date) + if (dateClass === "calendar-date") return + if (dateClass === "malformed") { throw new Error(`invalid date: ${fieldName} "${date}" (use YYYY-MM-DD)`) } - if (dateClass === "not-a-calendar-date") { - throw new Error( - `invalid date: ${fieldName} "${date}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, - ) - } + throw new Error( + `invalid date: ${fieldName} "${date}" is not a calendar date. Pass a real date in YYYY-MM-DD format.`, + ) } type ParentLocator = { kind: "blockId"; blockId: string } | { kind: "line"; line: number } @@ -1525,15 +1527,14 @@ const createTask = async (params: CreateTaskParams, logger: Logger): Promise Date: Fri, 9 Oct 2026 22:53:12 -0400 Subject: [PATCH 3/5] test: cover the scheduled and start date checks and match date errors exactly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createTask and updateTask: an impossible scheduled or start date is rejected with a message that names the field. No test covered these checks before, so dropping one let the date reach the note. - listTasks: the malformed date filter and missing .md extension tests compare the whole error message instead of a substring. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../search/__tests__/task-queries.test.ts | 4 +- .../__tests__/task-mutations.test.ts | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/vault-mcp/search/__tests__/task-queries.test.ts b/src/vault-mcp/search/__tests__/task-queries.test.ts index 817cc585d..ee7728c7f 100644 --- a/src/vault-mcp/search/__tests__/task-queries.test.ts +++ b/src/vault-mcp/search/__tests__/task-queries.test.ts @@ -445,7 +445,7 @@ describe("listTasks date filters", () => { it("rejects a malformed date with remediation text", () => { const index = createTestIndex() expect(() => index.listTasks({ due: { before: "July 3rd" } }, logger)).toThrow( - 'invalid due.before date: "July 3rd". Use YYYY-MM-DD (e.g. 2026-07-03).', + new Error('invalid due.before date: "July 3rd". Use YYYY-MM-DD (e.g. 2026-07-03).'), ) }) @@ -662,7 +662,7 @@ describe("listTasks scope filters", () => { it("rejects a path without the .md extension", () => { const index = createTestIndex() expect(() => index.listTasks({ path: "Inbox/notes" }, logger)).toThrow( - 'path must end in ".md" (received "Inbox/notes")', + new Error('path must end in ".md" (received "Inbox/notes")'), ) }) diff --git a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts index d96019af1..e41bf36bd 100644 --- a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts @@ -1282,6 +1282,41 @@ kanban-plugin: board ) }) + it.each([ + { + field: "scheduled", + dateParams: { scheduled: "2026-02-30" }, + expectedMessage: + 'invalid date: scheduled "2026-02-30" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + { + field: "start", + dateParams: { start: "2026-04-31" }, + expectedMessage: + 'invalid date: start "2026-04-31" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + ])( + "errors on a $field date the calendar lacks, naming the field", + async ({ dateParams, expectedMessage }) => { + const vault = await createVault() + await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) + + await expect( + taskMutations.createTask( + { + statusRegistry: DEFAULT_STATUS_REGISTRY, + vaultPath: vault, + path: "tasks.md", + description: "Bad date", + blockId: "bad-date", + ...dateParams, + }, + logger, + ), + ).rejects.toThrow(new Error(expectedMessage)) + }, + ) + it("errors when parent not found", async () => { const vault = await createVault() await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) @@ -2270,6 +2305,40 @@ kanban-plugin: board ) }) + it.each([ + { + field: "scheduled", + dateParams: { scheduled: "2026-02-30" }, + expectedMessage: + 'invalid date: scheduled "2026-02-30" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + { + field: "start", + dateParams: { start: "2026-04-31" }, + expectedMessage: + 'invalid date: start "2026-04-31" is not a calendar date. Pass a real date in YYYY-MM-DD format.', + }, + ])( + "errors on a $field date the calendar lacks in update, naming the field", + async ({ dateParams, expectedMessage }) => { + const vault = await createVault() + await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) + + await expect( + taskMutations.updateTask( + { + statusRegistry: DEFAULT_STATUS_REGISTRY, + vaultPath: vault, + path: "tasks.md", + blockId: "walk-dog", + ...dateParams, + }, + logger, + ), + ).rejects.toThrow(new Error(expectedMessage)) + }, + ) + it("errors on empty description in update", async () => { const vault = await createVault() await writeTestNote(vault, "tasks.md", SIMPLE_NOTE) From a97ce2c1120322d24e2a2b4b88c2c974482ab867 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:54:00 -0400 Subject: [PATCH 4/5] test: cover the created.after date check in vault_search's filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other created and modified bound already had a rejection test; a created.after bound naming a day the calendar lacks now does too. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- src/vault-mcp/search/__tests__/search-index.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vault-mcp/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index 95526d031..603186e20 100644 --- a/src/vault-mcp/search/__tests__/search-index.test.ts +++ b/src/vault-mcp/search/__tests__/search-index.test.ts @@ -6694,6 +6694,18 @@ Shared datefilter content for boundary tests. /^created\.before date "2026-02-31" is not a calendar date\. Pass a real date in YYYY-MM-DD format\.$/, ) }) + + it("rejects a created.after date the calendar lacks, naming that bound", () => { + const dateIndex = indexWithCreatedDates() + expect(() => { + dateIndex.fullTextSearch( + { query: "datefilter", filters: { created: { after: "2026-04-31" } } }, + logger, + ) + }).toThrow( + /^created\.after date "2026-04-31" is not a calendar date\. Pass a real date in YYYY-MM-DD format\.$/, + ) + }) }) describe("fullTextSearch modified filter", () => { From fd3c11004f1fde4db9095413bce095af7a75a196 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:01:02 -0400 Subject: [PATCH 5/5] test: match every task-mutation and hybrid-search error message exactly 28 assertions passed a string to toThrow, which matches any message that contains it. They now pass an Error, which vitest compares whole. Six of them named only part of the message and now name all of it. Co-Authored-By: Claude Opus 5.5 --- .../search/__tests__/hybrid-search.test.ts | 4 +- .../__tests__/task-mutations.test.ts | 96 +++++++++++++------ 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/vault-mcp/search/__tests__/hybrid-search.test.ts b/src/vault-mcp/search/__tests__/hybrid-search.test.ts index f72fa3fae..696d2d393 100644 --- a/src/vault-mcp/search/__tests__/hybrid-search.test.ts +++ b/src/vault-mcp/search/__tests__/hybrid-search.test.ts @@ -624,7 +624,9 @@ Content about quarterly planning and roadmaps. { query: "anything", filters: { modified: { on: "bad" } } }, logger, ), - ).rejects.toThrow('invalid modified.on date: "bad". Use YYYY-MM-DD (e.g. 2026-07-03).') + ).rejects.toThrow( + new Error('invalid modified.on date: "bad". Use YYYY-MM-DD (e.g. 2026-07-03).'), + ) }) }) diff --git a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts index e41bf36bd..8bfccd214 100644 --- a/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts @@ -754,7 +754,7 @@ title: Tasks }, logger, ), - ).rejects.toThrow('note not found: "missing.md"') + ).rejects.toThrow(new Error('note not found: "missing.md"')) }) it("reports a path through a file as not found, never the server's path", async () => { @@ -790,7 +790,7 @@ title: Tasks }, logger, ), - ).rejects.toThrow('blockId "nonexistent" not found') + ).rejects.toThrow(new Error('blockId "nonexistent" not found in "tasks.md"')) }) it("throws when line does not contain a task", async () => { @@ -808,7 +808,7 @@ title: Tasks }, logger, ), - ).rejects.toThrow("no task at line 1") + ).rejects.toThrow(new Error("no task at line 1")) }) it("throws when no mutations specified", async () => { @@ -842,7 +842,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('heading "Nonexistent" not found') + ).rejects.toThrow( + new Error('heading "Nonexistent" not found; available: Active, Up Next, Done'), + ) }) it("throws when multiple done lanes and no explicit heading", async () => { @@ -988,7 +990,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('hidden path blocked: ".trash/tasks.md" targets a hidden file or folder') + ).rejects.toThrow( + new Error('hidden path blocked: ".trash/tasks.md" targets a hidden file or folder'), + ) expect(await readTestNote(vault, ".trash/tasks.md")).toBe(SIMPLE_NOTE) }) }) @@ -1199,7 +1203,7 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow('blockId "walk-dog" already exists') + ).rejects.toThrow(new Error('blockId "walk-dog" already exists in this note')) }) it("errors on invalid block_id characters", async () => { @@ -1217,7 +1221,11 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow("contains invalid characters") + ).rejects.toThrow( + new Error( + 'blockId "bad id!" contains invalid characters (allowed: letters, digits, hyphens)', + ), + ) }) it("errors when heading is missing on a Kanban board", async () => { @@ -1333,7 +1341,7 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow("parent task not found") + ).rejects.toThrow(new Error('parent task not found: blockId "nonexistent"')) }) it("creates a sub-task under a parent identified by line number", async () => { @@ -1375,7 +1383,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - 'dependsOn entry " " contains invalid characters (allowed: letters, digits, hyphens, underscores)', + new Error( + 'dependsOn entry " " contains invalid characters (allowed: letters, digits, hyphens, underscores)', + ), ) const content = await readTestNote(vault, "tasks.md") expect(content).toBe(SIMPLE_NOTE) @@ -1398,7 +1408,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - 'taskId "has space" contains invalid characters (allowed: letters, digits, hyphens, underscores)', + new Error( + 'taskId "has space" contains invalid characters (allowed: letters, digits, hyphens, underscores)', + ), ) const content = await readTestNote(vault, "tasks.md") expect(content).toBe(SIMPLE_NOTE) @@ -1567,7 +1579,7 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow("parent task not found: line 1") + ).rejects.toThrow(new Error("parent task not found: line 1")) }) it("errors on empty description", async () => { @@ -1996,7 +2008,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - 'dependsOn entry " " contains invalid characters (allowed: letters, digits, hyphens, underscores)', + new Error( + 'dependsOn entry " " contains invalid characters (allowed: letters, digits, hyphens, underscores)', + ), ) const content = await readTestNote(vault, "tasks.md") expect(content).toBe(SIMPLE_NOTE) @@ -2161,7 +2175,7 @@ kanban-plugin: board }, logger, ), - ).rejects.toThrow('blockId "dup" already exists in this note') + ).rejects.toThrow(new Error('blockId "dup" already exists in this note')) const content = await readTestNote(vault, "tasks.md") expect(content).toBe("- [ ] Task A ➕ 2026-01-01 ^dup \n- [ ] Task B ➕ 2026-01-02 ^other\n") }) @@ -3434,7 +3448,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - `cannot reorder within "Tasks" — the heading appears 2 times; rename one section to make it unique`, + new Error( + `cannot reorder within "Tasks" — the heading appears 2 times; rename one section to make it unique`, + ), ) }) @@ -3456,7 +3472,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - `cannot place at position 1 under "Done" — the heading appears 2 times; rename one section to make it unique`, + new Error( + `cannot place at position 1 under "Done" — the heading appears 2 times; rename one section to make it unique`, + ), ) }) @@ -3503,7 +3521,9 @@ kanban-plugin: board logger, ), ).rejects.toThrow( - `cannot place at position 2 under "Tasks" — the heading appears 2 times; rename one section to make it unique`, + new Error( + `cannot place at position 2 under "Tasks" — the heading appears 2 times; rename one section to make it unique`, + ), ) }) @@ -4639,7 +4659,9 @@ title: Tasks logger, ), ).rejects.toThrow( - 'unrecognized recurrence rule "whenever I remember" (use the Tasks plugin\'s natural language, e.g. "every week", "every 2 weeks when done")', + new Error( + 'unrecognized recurrence rule "whenever I remember" (use the Tasks plugin\'s natural language, e.g. "every week", "every 2 weeks when done")', + ), ) }) @@ -4787,7 +4809,11 @@ title: Tasks }, logger, ), - ).rejects.toThrow('unrecognized recurrence rule "sometimes"') + ).rejects.toThrow( + new Error( + 'unrecognized recurrence rule "sometimes" (use the Tasks plugin\'s natural language, e.g. "every week", "every 2 weeks when done")', + ), + ) }) it("bases the recurrence on the next-priority date when the due date is calendar-invalid", async () => { @@ -6449,7 +6475,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry') + ).rejects.toThrow( + new Error('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry'), + ) }) it("updateTask rejects a NON_TASK checkbox by line number", async () => { @@ -6471,7 +6499,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry') + ).rejects.toThrow( + new Error('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry'), + ) }) it("createTask rejects a NON_TASK parent by block_id", async () => { @@ -6494,7 +6524,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry') + ).rejects.toThrow( + new Error('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry'), + ) }) it("createTask rejects a NON_TASK parent by line number", async () => { @@ -6517,7 +6549,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry') + ).rejects.toThrow( + new Error('checkbox "[>]" is a NON_TASK status in the Tasks plugin registry'), + ) }) it("allows updating a normal task when NON_TASK statuses exist", async () => { @@ -6608,7 +6642,9 @@ title: Tasks logger, ), ).rejects.toThrow( - 'no checkbox symbol for status "cancelled" in the Tasks plugin registry (the default "-" is typed todo)', + new Error( + 'no checkbox symbol for status "cancelled" in the Tasks plugin registry (the default "-" is typed todo)', + ), ) }) }) @@ -6706,7 +6742,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow('blockId "example" is inside a fenced code block or comment in "tasks.md"') + ).rejects.toThrow( + new Error('blockId "example" is inside a fenced code block or comment in "tasks.md"'), + ) }) it("createTask rejects a parent inside a fenced code block", async () => { @@ -6730,7 +6768,9 @@ title: Tasks logger, ), ).rejects.toThrow( - 'parent task not found: blockId "fenced" is inside a fenced code block or comment', + new Error( + 'parent task not found: blockId "fenced" is inside a fenced code block or comment', + ), ) }) @@ -6845,7 +6885,7 @@ title: Tasks }, logger, ), - ).rejects.toThrow("line 6 is inside a fenced code block or comment") + ).rejects.toThrow(new Error("line 6 is inside a fenced code block or comment")) }) it("createTask rejects a line-addressed parent inside a fenced code block", async () => { @@ -6868,7 +6908,9 @@ title: Tasks }, logger, ), - ).rejects.toThrow("parent task not found: line 6 is inside a fenced code block or comment") + ).rejects.toThrow( + new Error("parent task not found: line 6 is inside a fenced code block or comment"), + ) }) }) })