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..5576c79a7 --- /dev/null +++ b/src/utils/classify-iso-date.ts @@ -0,0 +1,21 @@ +import { DateTime } from "luxon" + +type IsoDateClass = "calendar-date" | "malformed" | "not-a-calendar-date" + +/** 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 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 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/__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..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,20 +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 -- A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") -- tag must not be empty after its leading "#" — a tags entry is 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" } }) @@ -61,11 +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 -- A malformed or calendar-invalid created/modified date filter throws with remediation text ("Use YYYY-MM-DD") -- tag must not be empty after its leading "#" — a tags entry is 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/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__/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/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index 8ef9d2db2..603186e20 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,19 @@ 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\.$/, + ) + }) + + 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\.$/, ) }) }) @@ -6787,7 +6799,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 +6810,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..ee7728c7f 100644 --- a/src/vault-mcp/search/__tests__/task-queries.test.ts +++ b/src/vault-mcp/search/__tests__/task-queries.test.ts @@ -445,14 +445,16 @@ 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).'), ) }) - 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.', + ), ) }) }) @@ -660,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/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index 8bb1bc6b9..9423a409d 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" @@ -140,18 +140,27 @@ type FileContentMetadataRow = { bytes: number } -// ── Full-text search ─────────────────────────────────────────── +// ── Date filter validation ───────────────────────────────────── -/** 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 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 => { - if (!DateTime.fromFormat(value, "yyyy-MM-dd").isValid) { + 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).`) } + 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: { @@ -260,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) @@ -996,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/__tests__/task-mutations.test.ts b/src/vault-mcp/vault-operations/__tests__/task-mutations.test.ts index 1f96bed37..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 () => { @@ -1240,7 +1248,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,9 +1283,48 @@ 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.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) @@ -1275,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 () => { @@ -1317,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) @@ -1340,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) @@ -1509,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 () => { @@ -1938,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) @@ -2103,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") }) @@ -2207,7 +2279,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,9 +2294,65 @@ 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.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) @@ -3320,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`, + ), ) }) @@ -3342,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`, + ), ) }) @@ -3389,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`, + ), ) }) @@ -4525,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")', + ), ) }) @@ -4673,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 () => { @@ -6335,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 () => { @@ -6357,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 () => { @@ -6380,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 () => { @@ -6403,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 () => { @@ -6494,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)', + ), ) }) }) @@ -6592,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 () => { @@ -6616,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', + ), ) }) @@ -6731,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 () => { @@ -6754,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"), + ) }) }) }) diff --git a/src/vault-mcp/vault-operations/task-mutations.ts b/src/vault-mcp/vault-operations/task-mutations.ts index 48ac83602..7157273e7 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" @@ -1404,11 +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 => { - if (!DateTime.fromFormat(date, "yyyy-MM-dd").isValid) { +/** 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)`) } + 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 } @@ -1517,15 +1527,14 @@ const createTask = async (params: CreateTaskParams, logger: Logger): Promise