Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
445a36a
fix(tools): point Errors entries at messages that carry their remedy,…
aliasunder Oct 25, 2026
91e91ce
fix(tools): apply tool-definition review findings to the Errors sweep
aliasunder Oct 25, 2026
c8af864
fix(tools): name the tool that protects the memory folder in vault_de…
aliasunder Oct 25, 2026
199b844
fix(tools): keep vault_delete_memory's routing true under any PROTECT…
aliasunder Oct 26, 2026
2b103f7
fix(tools): tell vault_delete_memory callers that vault_delete_note r…
aliasunder Oct 26, 2026
7b351b7
fix(tools): clarify description wording a first-time reader misread
aliasunder Oct 26, 2026
10a4d86
test: pin three behaviors the tool descriptions now state
aliasunder Oct 26, 2026
dfa7b82
fix(tools): correct three description claims the code does not keep
aliasunder Oct 26, 2026
9ee557c
fix(tools): say what status_char and done_lanes hold, and drop vault_…
aliasunder Oct 26, 2026
6d457b1
fix(tools): point vault_list_tasks' sort_direction at sort_by for the…
aliasunder Oct 26, 2026
146633e
fix(tools): explain the result fields and conditions a sweep found un…
aliasunder Oct 26, 2026
0a96983
fix(tools): drop vault_memory_recall's rerank condition and vault_lis…
aliasunder Oct 29, 2026
6e1b336
fix(tools): correct vault_update_task's delete entry and align vault_…
aliasunder Oct 29, 2026
08e3692
test: pin vault_search's folder as a file's full parent and a note's …
aliasunder Oct 29, 2026
c55fe95
fix(tools): state vault_update_task's delete condition and sort_direc…
aliasunder Oct 29, 2026
92aff1d
fix(tools): drop two repeated task-tool facts and state the done-lane…
aliasunder Oct 29, 2026
6aee731
fix(tools): name the tool that gives vault_list_memory_files' names, …
aliasunder Oct 29, 2026
3f4493d
fix(tools): answer Glama's remaining points on six tools
aliasunder Oct 29, 2026
e50061b
fix(tools): restore the Kanban indented-line exception to vault_updat…
aliasunder Oct 29, 2026
4fc410e
fix(tools): name vault_list_memory_files' heading fields in Returns
aliasunder Oct 29, 2026
df9dd98
test: pin vault_memory_recall's whole empty response
aliasunder Oct 29, 2026
47c9124
test: pin whole description lines and payloads, and each note discove…
aliasunder Oct 29, 2026
ed46e18
refactor(tools): share the path-safety, note-extension and concurrent…
aliasunder Oct 29, 2026
67b9c04
refactor(tools): name a sibling tool only while DISABLED_TOOLS leaves…
aliasunder Oct 29, 2026
4eb1bd5
fix(tools): name vault_replace_in_note in vault_update_memory's new-f…
aliasunder Oct 29, 2026
1b255c6
refactor(tools): share the remaining Errors entries two tools typed o…
aliasunder Oct 29, 2026
4770583
fix(tools): give vault_delete_note's protected-path error a remedy wh…
aliasunder Oct 29, 2026
16e50fa
docs: state that every tool cross-reference is availability-keyed, wi…
aliasunder Oct 29, 2026
406b4b0
fix(tools): cut description sentences that change no caller action
aliasunder Oct 29, 2026
e6ed94e
refactor(tools): tidy the shared Errors constants and gating helpers,…
aliasunder Oct 29, 2026
5d783ea
fix(tools): restore four caller facts the trim cut, and lower the siz…
aliasunder Oct 29, 2026
f275ee7
fix(tools): tell vault_update_task to let the listing catch up before…
aliasunder Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ mechanism-level detail.
index, OAuth database) remain.
- `DISABLED_TOOLS` narrows the surface tool-by-tool — e.g. keep writes
on but remove the delete tools. Same registration-time guarantee.
- Availability-keyed cross-references in surviving descriptions and
prompts disappear automatically. A small number of durable API-level
references remain (e.g. error-section alternatives naming sibling
tools).
- Cross-references in surviving descriptions and prompts are
availability-keyed and disappear with their target. A test disables
each tool in turn and checks that no served tool's description or
input schema still names it.
- Unknown tool names stop the server at startup rather than silently
disabling nothing.

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

325 changes: 227 additions & 98 deletions src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe("tool surface baseline", () => {
* - The allowance sits just above the default combo's average, so only
* small growth passes.
* Raising the allowance is a deliberate change with its reason in the PR. */
const CHARS_PER_TOOL_ALLOWANCE = 4198
const CHARS_PER_TOOL_ALLOWANCE = 3997

it.each(SIZE_CAPPED_COMBO_NAMES)(
"combo %s stays within the average per-tool size allowance",
Expand Down
67 changes: 41 additions & 26 deletions src/vault-mcp/mcp-core/tools/asset-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
describeTextWindow,
FILESYSTEM_ERROR_ENTRY,
LISTING_FILESYSTEM_ERROR_ENTRY,
PATH_SAFETY_ERROR_ENTRY,
} from "./tool-helpers.js"

/** One-line, model-facing summary accompanying an image block: what file it
Expand Down Expand Up @@ -80,53 +81,66 @@ export const registerAssetTools = ({
registerTool,
safeHandler,
safeHandlerContent,
isToolEnabled,
whenToolEnabledText,
vaultPath,
logger: sessionLogger,
config,
}: ToolRegistrationContext): void => {
// The contrast with the note tools names three of them, so it is kept only
// while all three are served; otherwise the plain purpose stands.
const noteDiscoveryToolsServed = [
TOOL_NAMES.VAULT_LIST_NOTES,
TOOL_NAMES.VAULT_SEARCH_BY_FOLDER,
TOOL_NAMES.VAULT_SEARCH,
].every(isToolEnabled)
const listFilesContrastSentence = noteDiscoveryToolsServed
? " vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else."
: " This is the discovery surface for every file that is not a markdown note."

registerTool(
TOOL_NAMES.VAULT_READ_FILE,
{
title: "Read File",
description: `Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.
description: `Read a non-markdown vault file in its most useful form per type${whenToolEnabledText("vault_read_note", " — the read-side companion to vault_read_note for everything that isn't a note")}.

Example: vault_read_file({ path: "attachments/diagram.png" }) — the image itself, shrunk when too large
Example: vault_read_file({ path: "Boards/Roadmap.canvas" }) — a readable outline of the canvas
Example: vault_read_file({ path: "exports/data.json" }) — the file content as text
Example: vault_read_file({ path: "exports/big.csv", limit: 500 }) — the first 500 lines
Example: vault_read_file({ path: "papers/research.pdf" }) — structured text with title, headings, and links

When to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF.${whenToolEnabledText("vault_get_outgoing_links", " Find the files a note links to (with byte sizes) via vault_get_outgoing_links.")}${whenToolEnabledText("vault_list_files", " Browse a folder's files via vault_list_files.")}${whenToolEnabledText("vault_search", " vault_search also indexes canvas, PDF, and text-format content, but not images or other files.")}${whenToolEnabledText("vault_read_note", " For .md notes use vault_read_note.")} To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.

What each file type returns:
- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, ${config.maxImageOutputBytes} bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.
- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.
- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.
- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.
- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (${config.maxImageOutputBytes} bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.
- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.
- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.
- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.

raw: true switches a canvas or PDF to its other form:
- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.
- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first ${config.maxPdfRenderPages} pages are rendered; later pages are readable only as text, without raw.
- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.
- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first ${config.maxPdfRenderPages} pages are rendered; later pages are readable only as text, without raw.

Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue ("data.csv — lines 51–100 of 400 (continue with start_line: 101)"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.

When to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.
Line paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue ("data.csv — lines 51–100 of 400 (continue with start_line: 101)"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.

Errors:
- "not a file: … is a markdown note" — the path ends in .md; read notes with vault_read_note
- "file not found" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files
- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)
- "not a file: … is a markdown note" — the path ends in .md${whenToolEnabledText("vault_read_note", "; read notes with vault_read_note")}
- "file not found" — no file exists at that path (a folder is not a file)${whenToolEnabledText("vault_list_files", "; discover valid paths via vault_list_files")}
${PATH_SAFETY_ERROR_ENTRY}
- "file too large" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise
- "text output too large" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows
- "start line past the end" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line
- "line range is not available" (start_line or limit on an image, or on a PDF read with raw: true) / "raw source is not available for images" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block
- "line range is not available" (start_line or limit on an image, or on a PDF read with raw: true) / "raw source is not available for images" (raw on an image) — drop that input
- "not valid UTF-8" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy
- "invalid .canvas JSON" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text
- "invalid .canvas JSON" — the canvas file is empty or not valid JSON; set raw: true to read its source as text
- "PDF has no extractable text" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead
- "PDF is password-protected" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead
- "PDF is damaged or not a PDF" / "could not decode image" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file
- "PDF page rendering failed" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF
- "image cannot be fitted" — the image could not be compressed under the image output budget; ask for a smaller export
- "PDF is password-protected" — this tool cannot supply a password; read an unprotected copy instead
- "PDF is damaged or not a PDF" / "could not decode image" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file
Comment thread
aliasunder marked this conversation as resolved.
Resolved
- "PDF page rendering failed" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF
- "image cannot be fitted into …" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES
${FILESYSTEM_ERROR_ENTRY}
- "file changed while reading" — the file was written while it was read (for example by Sync); retry the read
- "file changed while reading" — the file was written while it was read; retry the read
- "PDF reader could not start" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server
- "unsupported file type …" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size

Expand Down Expand Up @@ -227,18 +241,19 @@ Example: vault_list_files({}) — non-markdown files across the whole vault
Example: vault_list_files({ folder: "attachments" })
Example: vault_list_files({ extensions: [".png", ".jpg"], limit: 20 })

When to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.
When to use: discovering what files exist${whenToolEnabledText("vault_read_file", " before reading them with vault_read_file")}.${listFilesContrastSentence}${whenToolEnabledText("vault_get_outgoing_links", " For the files one specific note links to, prefer vault_get_outgoing_links.")}

Behavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.

Errors:
- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.
- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — the folder starts at the filesystem root, escapes the vault (e.g. "../elsewhere") or names the vault root itself (e.g. "."), or is hidden like ".obsidian" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.
- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.
- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — the folder is absolute, escapes the vault, names the vault root itself (e.g. "."), or is hidden like ".obsidian"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.
${LISTING_FILESYSTEM_ERROR_ENTRY}.

Returns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is "(none)" for a file without one), extension_counts (counts keyed by extension, e.g. { ".png": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).

Reading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.`,
Returns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is "(none)" for a file without one), extension_counts (counts keyed by extension, e.g. { ".png": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).${whenToolEnabledText(
"vault_read_file",
"\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.",
)}`,
inputSchema: {
folder: z
.string()
Expand Down
11 changes: 5 additions & 6 deletions src/vault-mcp/mcp-core/tools/daily-note-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { FILESYSTEM_ERROR_ENTRY } from "./tool-helpers.js"
export const registerDailyNoteTools = ({
registerTool,
safeHandler,
isToolEnabled,
whenToolEnabledText,
vaultPath,
logger: sessionLogger,
Expand All @@ -24,19 +23,19 @@ export const registerDailyNoteTools = ({
Example: vault_get_daily_note({ date: "2026-05-13" })
Example: vault_get_daily_note({}) — returns today's daily note

When to use: When you need today's or a specific date's daily note. ${whenToolEnabledText("vault_patch_note", "To append content to a daily note section, use the returned path with vault_patch_note. ")}Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).
When to use: When you need today's or a specific date's daily note.${whenToolEnabledText("vault_patch_note", " To append content to a daily note section, use the returned path with vault_patch_note.")}${whenToolEnabledText("vault_recent_notes", " For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.")}

Parameters:
- date: past and future dates are both valid.
- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.

Errors:
- "invalid date" — use YYYY-MM-DD format
- "… is not a calendar date" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date
- "daily note format contains unsupported token(s): ..." — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative
- "… is not a calendar date" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date
- "daily note format contains unsupported token(s): ..." — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix
- "path traversal blocked" / "hidden path blocked" / "absolute path blocked" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT
${FILESYSTEM_ERROR_ENTRY}

Returns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).${isToolEnabled("vault_write_note") ? " When exists is false, create the note with vault_write_note using the returned path." : ""}`,
Returns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).${whenToolEnabledText("vault_write_note", " When exists is false, create the note with vault_write_note using the returned path.")}`,
inputSchema: {
date: z
.string()
Expand Down
Loading
Loading