From ffc3e74fbdfa8f5486b6ac09e5bf1b789ecf2472 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:21:01 -0400 Subject: [PATCH 01/26] wip: daily notes root folder --- .../mcp-core/prompts/daily-review-prompt.ts | 2 +- .../prompts/vault-orientation-prompt.ts | 4 +- .../mcp-core/tools/daily-note-tools.ts | 2 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 4 +- .../mcp-core/tools/vault-crud-tools.ts | 2 +- .../__tests__/daily-notes.test.ts | 253 ++++++++++++++---- .../__tests__/vault-folder-config.test.ts | 41 +++ src/vault-mcp/vault-operations/daily-notes.ts | 173 +++++++++--- .../vault-operations/vault-folder-config.ts | 17 +- 9 files changed, 399 insertions(+), 99 deletions(-) diff --git a/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts b/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts index 1a886a6be..8e926215d 100644 --- a/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts @@ -221,7 +221,7 @@ export const registerDailyReviewPrompt = ({ ? search.getOutgoingLinks( { path: dailyNote.path, - dailyNotesFolder: dailyNotesConfig.folder, + dailyNotesFolder: dailyNotesConfig.dedicatedFolder, }, reqLogger, ) diff --git a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts index c435e3ed2..f08ede275 100644 --- a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts @@ -185,7 +185,7 @@ export const registerVaultOrientationPrompt = ({ excludeFolders: resolveEffectiveOrphanExcludeFolders({ orphanExcludeFoldersOverride: config.orphanExcludeFoldersOverride, memoryDir: config.memoryDir, - dailyNotesFolder: dailyNotesConfig.folder, + dailyNotesFolder: dailyNotesConfig.dedicatedFolder, }), limit: ORIENTATION_ORPHAN_LIMIT + 1, }, @@ -194,7 +194,7 @@ export const registerVaultOrientationPrompt = ({ const hasMoreOrphans = orphanResults.length > ORIENTATION_ORPHAN_LIMIT const orphans = orphanResults.slice(0, ORIENTATION_ORPHAN_LIMIT) const brokenLinkResult = search.brokenLinkCount( - { dailyNotesFolder: dailyNotesConfig.folder }, + { dailyNotesFolder: dailyNotesConfig.dedicatedFolder }, reqLogger, ) const stats = search.vaultStats({}, reqLogger) diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index 8c2edd28d..480860f8e 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -18,7 +18,7 @@ export const registerDailyNoteTools = ({ TOOL_NAMES.VAULT_GET_DAILY_NOTE, { title: "Get Daily Note", - description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to "Daily Notes" and YYYY-MM-DD. + description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to "Daily Notes" and YYYY-MM-DD. Example: vault_get_daily_note({ date: "2026-05-13" }) Example: vault_get_daily_note({}) — returns today's daily note diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 5dce95936..891b0644f 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -620,7 +620,7 @@ Errors: reqLogger, ) return search.getOutgoingLinks( - { path, dailyNotesFolder: dailyNotesConfig.folder }, + { path, dailyNotesFolder: dailyNotesConfig.dedicatedFolder }, reqLogger, ) }, @@ -641,7 +641,7 @@ Errors: : `the daily notes folder, "Templates", ${JSON.stringify(config.memoryDir)}` const orphanDefaultDescription = config.orphanExcludeFoldersOverride ? "With exclude_folders omitted, the server's configured list (the schema default) is used." - : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable).' + : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian\'s new-note folder.' // An override list may not include the daily notes folder, so the hint only // accompanies the built-in defaults. const dailyNotesFolderHint = config.orphanExcludeFoldersOverride diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 5a50195d2..5e50638a5 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -43,7 +43,7 @@ const describeProtectedPaths = (config: VaultConfig): string => { // loadConfig strips trailing slashes from override entries. return config.protectedPathsOverride.map((protectedPath) => protectedPath + "/").join(", ") } - return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/)` + return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/; none when daily notes share the vault root or Obsidian's new-note folder)` } /** Sentences that point to other tools, each already "" when its tool is diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index 93e1cac13..ae02ac206 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os" import { logger } from "../../../logger.js" import { readDailyNotesConfig, - readDailyNotesFileConfig, + readProtectedDailyNotesFolder, getDailyNotePath, getDailyNote, } from "../daily-notes.js" @@ -33,7 +33,11 @@ const jsonParseFailureMessage = (malformedJson: string): string => { throw new Error("expected JSON.parse to reject the malformed input") } -const FALLBACK_CONFIG = { folder: "Daily Notes", format: "YYYY-MM-DD" } +const FALLBACK_CONFIG = { + folder: "Daily Notes", + format: "YYYY-MM-DD", + dedicatedFolder: "Daily Notes", +} // ── readDailyNotesConfig ───────────────────────────────────────── @@ -44,7 +48,11 @@ describe("readDailyNotesConfig", () => { const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(config).toEqual({ folder: "Journal", format: "YYYY-MM-DD-dddd" }) + expect(config).toEqual({ + folder: "Journal", + format: "YYYY-MM-DD-dddd", + dedicatedFolder: "Journal", + }) }) it("falls back to defaults when file is missing", async () => { @@ -89,25 +97,112 @@ describe("readDailyNotesConfig", () => { expect(rootWarnSpy).not.toHaveBeenCalled() }) - it("uses default folder when config has empty folder string", async () => { + it("reads Obsidian's vault-root setting, a lone slash, as the vault root", async () => { const vaultDir = await createVault() - await writeDailyNotesConfig(vaultDir, { folder: "", format: "YYYY-MM-DD" }) + await writeDailyNotesConfig(vaultDir, { folder: "/", format: "YYYY-MM-DD" }) const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(config).toEqual(FALLBACK_CONFIG) + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) }) - it("reads a file holding the JSON literal null as no settings, without a warn", async () => { + it.each([ + { label: "leading and trailing slashes", setting: "/Journal/", expected: "Journal" }, + { label: "repeated slashes", setting: "Journal//2026", expected: "Journal/2026" }, + { label: "backslashes", setting: "Journal\\2026", expected: "Journal/2026" }, + ])("normalizes $label in the folder the way Obsidian does", async ({ setting, expected }) => { const vaultDir = await createVault() - await writeDailyNotesConfig(vaultDir, null) - const warnSpy = vi.spyOn(logger, "warn") - onTestFinished(() => warnSpy.mockRestore()) + await writeDailyNotesConfig(vaultDir, { folder: setting, format: "YYYY-MM-DD" }) const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(config).toEqual(FALLBACK_CONFIG) - expect(warnSpy).not.toHaveBeenCalled() + expect(config).toEqual({ folder: expected, format: "YYYY-MM-DD", dedicatedFolder: expected }) + }) + + describe("blank folder in daily-notes.json", () => { + const writeAppConfig = async (vaultDir: string, appConfig: unknown): Promise => { + await writeFile(join(vaultDir, ".obsidian", "app.json"), JSON.stringify(appConfig), "utf8") + } + + it("uses the vault root when app.json does not exist, as Obsidian's default", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "", format: "YYYY-MM-DD" }) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + }) + + it("uses the folder app.json sets for new notes, without treating it as daily-only", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { format: "DD-MM-YYYY" }) + await writeAppConfig(vaultDir, { newFileLocation: "folder", newFileFolderPath: "Inbox/" }) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "Inbox", format: "DD-MM-YYYY", dedicatedFolder: null }) + }) + + it("uses the vault root when new notes go next to the current file", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "" }) + await writeAppConfig(vaultDir, { newFileLocation: "current", newFileFolderPath: "Inbox" }) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + }) + + it("reads a file holding the JSON literal null as a blank folder, without a warn", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, null) + await writeAppConfig(vaultDir, { newFileLocation: "folder", newFileFolderPath: "Inbox" }) + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "Inbox", format: "YYYY-MM-DD", dedicatedFolder: null }) + expect(warnSpy).not.toHaveBeenCalled() + }) + + it("uses the vault root on a malformed app.json and warns on the caller's logger", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "" }) + const malformedJson = "not valid json{{{" + await writeFile(join(vaultDir, ".obsidian", "app.json"), malformedJson, "utf8") + const requestLogger = logger.child({ requestId: "request-1" }) + const requestWarnSpy = vi.spyOn(requestLogger, "warn") + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, requestLogger) + + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + expect(requestWarnSpy).toHaveBeenCalledTimes(1) + expect(requestWarnSpy).toHaveBeenCalledWith( + "cannot read new note location, using the vault root", + { error: `[SyntaxError]: ${jsonParseFailureMessage(malformedJson)}` }, + ) + }) + + it("lets a folder override win without reading app.json", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "", format: "DD-MM-YYYY" }) + await writeFile(join(vaultDir, ".obsidian", "app.json"), "not valid json{{{", "utf8") + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) + + const config = await readDailyNotesConfig( + { vaultPath: vaultDir, envSettings: { folder: "Override Folder" } }, + logger, + ) + + expect(config).toEqual({ + folder: "Override Folder", + format: "DD-MM-YYYY", + dedicatedFolder: "Override Folder", + }) + expect(warnSpy).not.toHaveBeenCalled() + }) }) it("uses default format when config has empty format string", async () => { @@ -116,7 +211,7 @@ describe("readDailyNotesConfig", () => { const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(config).toEqual({ folder: "Journal", format: "YYYY-MM-DD" }) + expect(config).toEqual({ folder: "Journal", format: "YYYY-MM-DD", dedicatedFolder: "Journal" }) }) it("follows a folder and format change between two reads", async () => { @@ -124,11 +219,11 @@ describe("readDailyNotesConfig", () => { await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "YYYY-MM-DD" }) const first = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(first).toEqual({ folder: "Journal", format: "YYYY-MM-DD" }) + expect(first).toEqual({ folder: "Journal", format: "YYYY-MM-DD", dedicatedFolder: "Journal" }) await writeDailyNotesConfig(vaultDir, { folder: "Changed", format: "DD-MM-YYYY" }) const second = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(second).toEqual({ folder: "Changed", format: "DD-MM-YYYY" }) + expect(second).toEqual({ folder: "Changed", format: "DD-MM-YYYY", dedicatedFolder: "Changed" }) }) it("picks up a config file that appears after an earlier read", async () => { @@ -139,7 +234,11 @@ describe("readDailyNotesConfig", () => { await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "DD-MM-YYYY" }) const afterFileExists = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(afterFileExists).toEqual({ folder: "Journal", format: "DD-MM-YYYY" }) + expect(afterFileExists).toEqual({ + folder: "Journal", + format: "DD-MM-YYYY", + dedicatedFolder: "Journal", + }) }) it("picks up a repaired config file after a malformed read", async () => { @@ -154,7 +253,11 @@ describe("readDailyNotesConfig", () => { await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "DD-MM-YYYY" }) const afterFix = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) - expect(afterFix).toEqual({ folder: "Journal", format: "DD-MM-YYYY" }) + expect(afterFix).toEqual({ + folder: "Journal", + format: "DD-MM-YYYY", + dedicatedFolder: "Journal", + }) expect(warnSpy).toHaveBeenCalledTimes(1) }) @@ -168,7 +271,11 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Override Folder", format: "DD-MM-YYYY" }) + expect(config).toEqual({ + folder: "Override Folder", + format: "DD-MM-YYYY", + dedicatedFolder: "Override Folder", + }) }) it("format-only override wins over the file's format, file keeps folder", async () => { @@ -180,7 +287,11 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Journal", format: "YYYY_MM_DD" }) + expect(config).toEqual({ + folder: "Journal", + format: "YYYY_MM_DD", + dedicatedFolder: "Journal", + }) }) it("both overrides win over a conflicting file", async () => { @@ -192,7 +303,11 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Override Folder", format: "YYYY_MM_DD" }) + expect(config).toEqual({ + folder: "Override Folder", + format: "YYYY_MM_DD", + dedicatedFolder: "Override Folder", + }) }) it("both overrides skip the file, so a malformed one is never read", async () => { @@ -206,7 +321,11 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Override Folder", format: "YYYY_MM_DD" }) + expect(config).toEqual({ + folder: "Override Folder", + format: "YYYY_MM_DD", + dedicatedFolder: "Override Folder", + }) expect(warnSpy).not.toHaveBeenCalled() }) @@ -218,7 +337,11 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Override Folder", format: "YYYY_MM_DD" }) + expect(config).toEqual({ + folder: "Override Folder", + format: "YYYY_MM_DD", + dedicatedFolder: "Override Folder", + }) }) it("folder-only override without a config file falls back to the default format", async () => { @@ -229,53 +352,67 @@ describe("readDailyNotesConfig", () => { logger, ) - expect(config).toEqual({ folder: "Override Folder", format: "YYYY-MM-DD" }) + expect(config).toEqual({ + folder: "Override Folder", + format: "YYYY-MM-DD", + dedicatedFolder: "Override Folder", + }) }) }) }) -// ── readDailyNotesFileConfig ───────────────────────────────────── +// ── readProtectedDailyNotesFolder ──────────────────────────────── -describe("readDailyNotesFileConfig", () => { - it("reads folder and format from .obsidian/daily-notes.json", async () => { +describe("readProtectedDailyNotesFolder", () => { + it("reads the folder from .obsidian/daily-notes.json", async () => { const vaultDir = await createVault() - await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "DD-MM-YYYY" }) + await writeDailyNotesConfig(vaultDir, { folder: "Journal/", format: "DD-MM-YYYY" }) - const config = await readDailyNotesFileConfig(vaultDir, logger) + const folder = await readProtectedDailyNotesFolder(vaultDir, logger) - expect(config).toEqual({ folder: "Journal", format: "DD-MM-YYYY" }) + expect(folder).toBe("Journal") }) - it("falls back to defaults when the file is missing", async () => { + it("falls back to Daily Notes when the file is missing", async () => { const vaultDir = await createVault() const warnSpy = vi.spyOn(logger, "warn") onTestFinished(() => warnSpy.mockRestore()) - const config = await readDailyNotesFileConfig(vaultDir, logger) + const folder = await readProtectedDailyNotesFolder(vaultDir, logger) - expect(config).toEqual(FALLBACK_CONFIG) + expect(folder).toBe("Daily Notes") expect(warnSpy).not.toHaveBeenCalled() }) - it("falls back to defaults when .obsidian is a file rather than a folder", async () => { + it("falls back to Daily Notes when .obsidian is a file rather than a folder", async () => { const vaultDir = await mkdtemp(join(tmpdir(), "daily-notes-test-")) onTestFinished(async () => rm(vaultDir, { recursive: true })) await writeFile(join(vaultDir, ".obsidian"), "not a folder", "utf8") - const config = await readDailyNotesFileConfig(vaultDir, logger) + const folder = await readProtectedDailyNotesFolder(vaultDir, logger) - expect(config).toEqual(FALLBACK_CONFIG) + expect(folder).toBe("Daily Notes") }) - it("reads a file holding the JSON literal null as no settings, not as unreadable", async () => { + it.each([ + { label: "the vault root", config: { folder: "/" } }, + { label: "a blank folder (Obsidian's new-note location)", config: { folder: "" } }, + { label: "the JSON literal null (a blank folder)", config: null }, + ])("protects no folder for $label", async ({ config }) => { const vaultDir = await createVault() - await writeDailyNotesConfig(vaultDir, null) + await writeDailyNotesConfig(vaultDir, config) + // A new-note folder in app.json must not become the protected folder + await writeFile( + join(vaultDir, ".obsidian", "app.json"), + JSON.stringify({ newFileLocation: "folder", newFileFolderPath: "Inbox" }), + "utf8", + ) const warnSpy = vi.spyOn(logger, "warn") onTestFinished(() => warnSpy.mockRestore()) - const config = await readDailyNotesFileConfig(vaultDir, logger) + const folder = await readProtectedDailyNotesFolder(vaultDir, logger) - expect(config).toEqual(FALLBACK_CONFIG) + expect(folder).toBeNull() expect(warnSpy).not.toHaveBeenCalled() }) @@ -289,7 +426,7 @@ describe("readDailyNotesFileConfig", () => { const rootWarnSpy = vi.spyOn(logger, "warn") onTestFinished(() => rootWarnSpy.mockRestore()) - await expect(readDailyNotesFileConfig(vaultDir, requestLogger)).rejects.toThrow( + await expect(readProtectedDailyNotesFolder(vaultDir, requestLogger)).rejects.toThrow( new Error("cannot read daily notes config from .obsidian/daily-notes.json"), ) @@ -306,7 +443,7 @@ describe("readDailyNotesFileConfig", () => { const warnSpy = vi.spyOn(logger, "warn") onTestFinished(() => warnSpy.mockRestore()) - await expect(readDailyNotesFileConfig(vaultDir, logger)).rejects.toThrow( + await expect(readProtectedDailyNotesFolder(vaultDir, logger)).rejects.toThrow( new Error("cannot read daily notes config from .obsidian/daily-notes.json"), ) @@ -320,12 +457,12 @@ describe("readDailyNotesFileConfig", () => { const vaultDir = await createVault() await writeDailyNotesConfig(vaultDir, { folder: "Journal" }) - const first = await readDailyNotesFileConfig(vaultDir, logger) - expect(first).toEqual({ folder: "Journal", format: "YYYY-MM-DD" }) + const first = await readProtectedDailyNotesFolder(vaultDir, logger) + expect(first).toBe("Journal") await writeDailyNotesConfig(vaultDir, { folder: "Diary" }) - const second = await readDailyNotesFileConfig(vaultDir, logger) - expect(second).toEqual({ folder: "Diary", format: "YYYY-MM-DD" }) + const second = await readProtectedDailyNotesFolder(vaultDir, logger) + expect(second).toBe("Diary") }) }) @@ -349,6 +486,32 @@ describe("getDailyNotePath", () => { expect(path).toBe("Journal/13-05-2026.md") }) + it("resolves a path at the vault root when daily notes live there", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "/", format: "DD-MM-YYYY" }) + + const path = await getDailyNotePath({ vaultPath: vaultDir, date: "2026-05-13" }, logger) + + expect(path).toBe("13-05-2026.md") + }) + + it("resolves a vault-root path from a resolved config passed back as settings", async () => { + const vaultDir = await createVault() + // A folder setting the shortcut must skip, so a file read would change the path + await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "YYYY-MM-DD" }) + + const path = await getDailyNotePath( + { + vaultPath: vaultDir, + date: "2026-05-13", + envSettings: { folder: "", format: "DD-MM-YYYY" }, + }, + logger, + ) + + expect(path).toBe("13-05-2026.md") + }) + it("resolves with env overrides winning over the config file", async () => { const vaultDir = await createVault() await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "YYYY-MM-DD" }) diff --git a/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts b/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts index 8889d9335..798cd6387 100644 --- a/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts @@ -58,6 +58,20 @@ describe("resolveEffectiveProtectedPaths", () => { ).toEqual(["About Me"]) }) + it.each([ + { label: "daily notes in the vault root", settings: '{"folder":"/"}' }, + { label: "daily notes in Obsidian's new-note folder", settings: '{"folder":""}' }, + ])("protects only memory for $label", async ({ settings }) => { + const vaultPath = await createVault(settings) + await writeFile( + join(vaultPath, ".obsidian/app.json"), + '{"newFileLocation":"folder","newFileFolderPath":"Inbox"}', + ) + expect( + await resolveEffectiveProtectedPaths({ config: loadConfig({}), vaultPath }, logger), + ).toEqual(["About Me"]) + }) + it("preserves spaces in a nonblank file-configured daily folder", async () => { const vaultPath = await createVault('{"folder":" Journal "}') expect( @@ -143,6 +157,16 @@ describe("resolveEffectiveOrphanExcludeFolders", () => { ).toEqual(["Templates", "About Me"]) }) + it("omits the daily folder when daily notes share a folder with other notes", () => { + expect( + resolveEffectiveOrphanExcludeFolders({ + orphanExcludeFoldersOverride: null, + memoryDir: "About Me", + dailyNotesFolder: null, + }), + ).toEqual(["Templates", "About Me"]) + }) + it("preserves an explicit empty list", () => { expect( resolveEffectiveOrphanExcludeFolders({ @@ -186,8 +210,25 @@ describe("readEffectiveOrphanExcludeFolders", () => { env: {}, expected: ["Templates", "About Me"], }, + { + label: "vault-root file folder", + settings: '{"folder":"/"}', + env: {}, + expected: ["Templates", "About Me"], + }, + { + label: "empty file folder, which follows the new-note folder", + settings: '{"folder":""}', + env: {}, + expected: ["Templates", "About Me"], + }, ])("uses $label", async ({ settings, env, expected }) => { const vaultPath = await createVault(settings) + // The new-note folder is where a blank daily folder resolves; it must not be excluded + await writeFile( + join(vaultPath, ".obsidian/app.json"), + '{"newFileLocation":"folder","newFileFolderPath":"Inbox"}', + ) expect( await readEffectiveOrphanExcludeFolders({ config: loadConfig(env), vaultPath }, logger), ).toEqual(expected) diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index 0694106ce..e0c11f8be 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -5,19 +5,39 @@ import type { Logger } from "../../logger.js" import { vaultFs } from "./vault-filesystem.js" import { momentToLuxonFormat, findUnsupportedTokens } from "../obsidian-markdown/moment-format.js" import { describeError } from "../../utils/describe-error.js" -import { isMissingPathError } from "../../utils/fs.js" +import { isMissingPathError, readFileOrNull } from "../../utils/fs.js" import { isRecord } from "../../utils/is-record.js" // ── Config reading ────────────────────────────────────────────── // // Three readers, each wrapping readDailyNotesFile: -// readDailyNotesFileConfig — strict: throws on an unreadable file (for the delete/move guard) -// readDailyNotesFileConfigOrFallback — tolerant: warns + falls back (for reads and prompts) -// readDailyNotesConfig — public: env → file → fallbacks, uses the tolerant reader +// readProtectedDailyNotesFolder — strict: throws on an unreadable file (for the delete/move guard) +// readDailyNotesFileOrFallback — tolerant: warns + falls back (for reads and prompts) +// readDailyNotesConfig — public: env → file → fallbacks, uses the tolerant reader type DailyNotesConfig = { + /** Where daily notes are read from: vault-relative, with no leading or + * trailing slash; "" is the vault root. */ folder: string format: string + /** The folder when it holds daily notes apart from other notes, for the + * delete guard, orphan exclusion and forward-reference counts. Null when + * daily notes sit in the vault root or Obsidian's new-note folder, since + * treating those as daily-only would cover every other note there too. */ + dedicatedFolder: string | null +} + +/** The folder setting in daily-notes.json: + * - `no-file`: the file does not exist. + * - `blank`: the file exists with a missing or empty folder, so Obsidian + * puts daily notes in its location for new notes. + * - `named`: the normalized folder; "" is the vault root. */ +type DailyNotesFolderSetting = + { kind: "no-file" } | { kind: "blank" } | { kind: "named"; folder: string } + +type DailyNotesFileSettings = { + folderSetting: DailyNotesFolderSetting + format: string } /** Per-field settings from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env @@ -29,18 +49,30 @@ export type DailyNotesEnvSettings = { export const DEFAULT_DAILY_NOTES_FOLDER = "Daily Notes" +const DEFAULT_DAILY_NOTES_FORMAT = "YYYY-MM-DD" + // The format matches Obsidian's default; the folder is this server's own -// choice — Obsidian with no configured location creates dailies in the -// vault root, which is not a sensible folder for the server to assume. -const FALLBACK_CONFIG: DailyNotesConfig = { - folder: DEFAULT_DAILY_NOTES_FOLDER, - format: "YYYY-MM-DD", +// choice — Obsidian with no daily-notes.json creates dailies in its location +// for new notes, which is not a sensible folder for the server to assume. +const FALLBACK_FILE_SETTINGS: DailyNotesFileSettings = { + folderSetting: { kind: "no-file" }, + format: DEFAULT_DAILY_NOTES_FORMAT, } -/** Reads and parses .obsidian/daily-notes.json. Returns the fallbacks when - * no file exists there, and for each field the file does not set; any - * other failure propagates. */ -const readDailyNotesFile = async (vaultPath: string): Promise => { +/** Matches each run of forward or back slashes in a folder setting. */ +const SLASH_RUN_RE = /[\\/]+/g + +/** Normalizes a folder setting the way Obsidian does before using it: + * slash runs (either direction) become one "/", and leading and trailing + * slashes are dropped. Obsidian saves the vault root as "/", which + * becomes "". */ +const normalizeFolderSetting = (folderSetting: string): string => { + return folderSetting.replace(SLASH_RUN_RE, "/").replace(/^\/|\/$/g, "") +} + +/** Reads and parses .obsidian/daily-notes.json. A missing file gives the + * `no-file` setting; any other failure propagates. */ +const readDailyNotesFile = async (vaultPath: string): Promise => { try { // Read on every call, so a folder or format change in Obsidian applies // to the next operation rather than after a restart. @@ -51,34 +83,43 @@ const readDailyNotesFile = async (vaultPath: string): Promise const parsedConfig: unknown = JSON.parse(configFileContent) // Valid JSON can be `null` or a bare value, which has no keys to read. const settings = isRecord(parsedConfig) ? parsedConfig : {} + const folderSetting: DailyNotesFolderSetting = + typeof settings.folder === "string" && settings.folder.length > 0 + ? { kind: "named", folder: normalizeFolderSetting(settings.folder) } + : { kind: "blank" } return { - folder: - typeof settings.folder === "string" && settings.folder.length > 0 - ? settings.folder - : FALLBACK_CONFIG.folder, + folderSetting, format: typeof settings.format === "string" && settings.format.length > 0 ? settings.format - : FALLBACK_CONFIG.format, + : DEFAULT_DAILY_NOTES_FORMAT, } } catch (error) { // No file, or no .obsidian/ folder at all: on a fresh remote deploy the // config can arrive after boot, so the fallbacks stand in until it does. - if (isMissingPathError(error)) return { ...FALLBACK_CONFIG } + if (isMissingPathError(error)) return FALLBACK_FILE_SETTINGS throw error } } -/** Reads the folder and filename format from .obsidian/daily-notes.json, for - * the protected-path guard. - * - Returns the fallbacks ("Daily Notes", "YYYY-MM-DD") when the file is missing. +const resolveDedicatedFolder = (folderSetting: DailyNotesFolderSetting): string | null => { + if (folderSetting.kind === "no-file") return DEFAULT_DAILY_NOTES_FOLDER + if (folderSetting.kind === "blank") return null + return folderSetting.folder || null +} + +/** Reads the daily notes folder the delete and move guard protects, from + * .obsidian/daily-notes.json. + * - Returns "Daily Notes" when the file is missing, and null when daily + * notes sit in the vault root or Obsidian's new-note folder. * - Throws when the file exists but cannot be read or parsed. */ -export const readDailyNotesFileConfig = async ( +export const readProtectedDailyNotesFolder = async ( vaultPath: string, logger: Logger, -): Promise => { +): Promise => { try { - return await readDailyNotesFile(vaultPath) + const { folderSetting } = await readDailyNotesFile(vaultPath) + return resolveDedicatedFolder(folderSetting) } catch (error) { // A fallback here would protect "Daily Notes" while the user's folder is // whatever the broken file says. The warn carries the raw cause; only the @@ -90,28 +131,71 @@ export const readDailyNotesFileConfig = async ( } } -/** The file config, or the fallbacks with a `warn` when the file exists but +/** The file settings, or the fallbacks with a `warn` when the file exists but * cannot be read or parsed — for reads and prompts, where a wrong folder * costs a missed note, not a deleted one. */ -const readDailyNotesFileConfigOrFallback = async ( +const readDailyNotesFileOrFallback = async ( vaultPath: string, logger: Logger, -): Promise => { +): Promise => { try { return await readDailyNotesFile(vaultPath) } catch (error) { logger.warn("cannot read daily notes config, using defaults", { error: describeError(error), }) - return { ...FALLBACK_CONFIG } + return FALLBACK_FILE_SETTINGS } } +/** Obsidian's location for new notes, read from `newFileLocation` and + * `newFileFolderPath` in .obsidian/app.json. + * - Returns the folder when new notes go to a set folder, else the vault + * root (""). "Same folder as current file" also means the root, since a + * daily note has no current file. + * - A missing or unreadable app.json means Obsidian's default, the root. + * - Unlike Obsidian, a set folder that no longer exists is not swapped for + * the root; the daily note then reads as missing. */ +const readNewNoteFolder = async (vaultPath: string, logger: Logger): Promise => { + try { + const appConfigContent = await readFileOrNull(join(vaultPath, ".obsidian", "app.json")) + + if (appConfigContent === null) return "" + + const parsedConfig: unknown = JSON.parse(appConfigContent) + const settings = isRecord(parsedConfig) ? parsedConfig : {} + const { newFileLocation, newFileFolderPath } = settings + + if (newFileLocation === "folder" && typeof newFileFolderPath === "string") { + return normalizeFolderSetting(newFileFolderPath) + } + return "" + } catch (error) { + // Reads and prompts only, so a wrong folder costs a missed note. + logger.warn("cannot read new note location, using the vault root", { + error: describeError(error), + }) + return "" + } +} + +const resolveDailyNotesFolder = async ( + { folderSetting, vaultPath }: { folderSetting: DailyNotesFolderSetting; vaultPath: string }, + logger: Logger, +): Promise => { + if (folderSetting.kind === "no-file") return DEFAULT_DAILY_NOTES_FOLDER + if (folderSetting.kind === "blank") return readNewNoteFolder(vaultPath, logger) + return folderSetting.folder +} + /** Resolves the vault's daily note folder and filename format with * per-field precedence: env setting → .obsidian/daily-notes.json → - * the fallbacks ("Daily Notes", "YYYY-MM-DD"). When both fields are - * set via env the config file is not read at all. A file that exists but - * cannot be read or parsed logs a `warn` and counts as absent. */ + * the fallbacks ("Daily Notes", "YYYY-MM-DD"). + * - A blank folder in daily-notes.json means Obsidian's location for new + * notes, read from .obsidian/app.json. + * - When both fields are set via env, no config file is read. + * - A daily-notes.json that exists but cannot be read or parsed logs a + * `warn` and counts as absent. */ export const readDailyNotesConfig = async ( params: { vaultPath: string @@ -120,17 +204,26 @@ export const readDailyNotesConfig = async ( logger: Logger, ): Promise => { const { vaultPath, envSettings } = params + const envFolder = envSettings?.folder - // Both fields set via env — the file can't contribute anything, skip I/O. - if (envSettings?.folder && envSettings.format) { - return { folder: envSettings.folder, format: envSettings.format } + // Both fields set — the files can't contribute anything, skip I/O. A + // resolved config passed back in can carry "" (the vault root) as its folder. + if (envFolder !== undefined && envSettings?.format) { + return { folder: envFolder, format: envSettings.format, dedicatedFolder: envFolder || null } } - const fileConfig = await readDailyNotesFileConfigOrFallback(vaultPath, logger) - return { - folder: envSettings?.folder ?? fileConfig.folder, - format: envSettings?.format ?? fileConfig.format, + const { folderSetting, format: fileFormat } = await readDailyNotesFileOrFallback( + vaultPath, + logger, + ) + const format = envSettings?.format ?? fileFormat + + if (envFolder !== undefined) { + return { folder: envFolder, format, dedicatedFolder: envFolder || null } } + + const folder = await resolveDailyNotesFolder({ folderSetting, vaultPath }, logger) + return { folder, format, dedicatedFolder: resolveDedicatedFolder(folderSetting) } } // ── Path resolution + read ────────────────────────────────────── @@ -183,7 +276,7 @@ export const getDailyNotePath = async ( } const filename = dateTime.toFormat(luxonFormat) - return `${config.folder}/${filename}.md` + return config.folder ? `${config.folder}/${filename}.md` : `${filename}.md` } type DailyNoteResult = { diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index 6aa86e4c8..58932ae87 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -1,6 +1,6 @@ import type { VaultConfig } from "../config.js" import type { Logger } from "../../logger.js" -import { readDailyNotesConfig, readDailyNotesFileConfig } from "./daily-notes.js" +import { readDailyNotesConfig, readProtectedDailyNotesFolder } from "./daily-notes.js" /** The folders that delete and move refuse to touch. * - PROTECTED_PATHS, when set, replaces the defaults entirely, so only the @@ -9,6 +9,9 @@ import { readDailyNotesConfig, readDailyNotesFileConfig } from "./daily-notes.js * plus the daily notes folder, resolved on each call (DAILY_NOTES_FOLDER → * .obsidian/daily-notes.json → "Daily Notes") so a folder configured only * in the vault is protected too. + * - Daily notes kept in the vault root or Obsidian's new-note folder are not + * protected, since protecting either folder would cover every other note + * in it. * Throws when daily-notes.json exists but cannot be read, since the folder * it names is then unknown; DAILY_NOTES_FOLDER or PROTECTED_PATHS bypasses * the file. */ @@ -21,11 +24,10 @@ export const resolveEffectiveProtectedPaths = async ( // loadConfig trims DAILY_NOTES_FOLDER, so a set value is never blank. if (config.dailyNotesFolder) return [config.memoryDir, config.dailyNotesFolder] - const dailyNotesConfig = await readDailyNotesFileConfig(vaultPath, logger) + const dailyFolder = await readProtectedDailyNotesFolder(vaultPath, logger) // A whitespace-only folder in daily-notes.json protects nothing. - const dailyFolder = dailyNotesConfig.folder.replace(/\/+$/, "") - return dailyFolder.trim() ? [config.memoryDir, dailyFolder] : [config.memoryDir] + return dailyFolder?.trim() ? [config.memoryDir, dailyFolder] : [config.memoryDir] } export const resolveEffectiveOrphanExcludeFolders = ({ @@ -35,12 +37,13 @@ export const resolveEffectiveOrphanExcludeFolders = ({ }: { orphanExcludeFoldersOverride: readonly string[] | null memoryDir: string - dailyNotesFolder: string + /** Null when daily notes share the vault root or the new-note folder with other notes. */ + dailyNotesFolder: string | null }): readonly string[] => { if (orphanExcludeFoldersOverride) return orphanExcludeFoldersOverride /** Whitespace-only settings exclude no daily folder; spaces in a nonblank folder name stay. */ - return dailyNotesFolder.trim() + return dailyNotesFolder?.trim() ? [dailyNotesFolder, "Templates", memoryDir] : ["Templates", memoryDir] } @@ -64,6 +67,6 @@ export const readEffectiveOrphanExcludeFolders = async ( return resolveEffectiveOrphanExcludeFolders({ orphanExcludeFoldersOverride: null, memoryDir: config.memoryDir, - dailyNotesFolder: dailyNotesConfig.folder, + dailyNotesFolder: dailyNotesConfig.dedicatedFolder, }) } From 8781515d88f239a6159fa7de0e93072f548dc792 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:23:26 -0400 Subject: [PATCH 02/26] fix(daily-notes): regenerate the tool surface and pin the new description text Co-Authored-By: Claude Opus 5.5 --- .../__tests__/__snapshots__/tool-surface/default.json | 8 ++++---- .../__snapshots__/tool-surface/disabled-tools.json | 8 ++++---- .../__snapshots__/tool-surface/embedding-off.json | 8 ++++---- .../tool-surface/file-tools-off+embedding-off.json | 8 ++++---- .../__snapshots__/tool-surface/file-tools-off.json | 8 ++++---- .../tool-surface/memory-off+embedding-off.json | 8 ++++---- .../memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/memory-off+file-tools-off.json | 8 ++++---- .../__tests__/__snapshots__/tool-surface/memory-off.json | 8 ++++---- .../__snapshots__/tool-surface/obsidian-sync.json | 8 ++++---- .../tool-surface/readonly+embedding-off.json | 4 ++-- .../readonly+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+memory-off+file-tools-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly+memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/readonly.json | 4 ++-- src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts | 8 ++++---- .../mcp-core/__tests__/tool-surface-snapshot.test.ts | 2 +- 20 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 2519b62c0..6ef67a370 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 @@ -194,7 +194,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -334,7 +334,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 29f788b54..07ecc8ea6 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 effc9efd8..96f7435b8 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 92905468a..5256cb8e4 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 @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -337,7 +337,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 d284c4b8c..c696793b7 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 8b639743f..e2b003052 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 ffac4f220..aa7e7dafa 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 @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -292,7 +292,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 f11266c8d..2ce4b76d2 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 6e7ef8f03..1b632b62a 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 @@ -150,7 +150,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -290,7 +290,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 746a63c04..c265362d8 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 9bfacb887..7379aac87 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 6e3a5baf8..ccedf2583 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 075239886..f534af793 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 bc7f83bdf..01dea62c5 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 e0fed47f2..3f954ebf2 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 d6253d90a..32dfbde6c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 c09c73e4e..28bbc4886 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 dd134ed8c..216ebb27f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable).\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -71,7 +71,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 1db767a7a..ccbf692e4 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -517,7 +517,7 @@ describe("config interpolation in descriptions", () => { linePrefix: "- Links to the note", }) expect(linksEntry).toBe( - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/).", + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).", ) }) @@ -1158,7 +1158,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { }) } const LINKS_AND_PROTECTED_PATHS_ENTRY = - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)." + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)." it("describes the Deleted files outcomes when the server does not sync", () => { expect(deleteNoteOpener({})).toBe( @@ -1838,7 +1838,7 @@ describe("vault_find_orphans live folder defaults", () => { ?.split("\n") .find((line) => line.startsWith("- With exclude_folders")) expect(defaultsLine).toBe( - '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable).', + '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian\'s new-note folder.', ) expect(toolConfig.description).not.toContain("Journal") expect(toolConfig.inputSchema?.exclude_folders?.description).toBe( @@ -2629,7 +2629,7 @@ describe("DISABLED_TOOLS", () => { }) } const PROTECTED_PATHS_SENTENCE = - " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)." + " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)." expect(linksEntry("")).toBe( `- Links to the note from other notes become broken; list them first with vault_get_backlinks.${PROTECTED_PATHS_SENTENCE}`, diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index 0b19f6954..4bdc850f3 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4234 + const CHARS_PER_TOOL_ALLOWANCE = 4235 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", From 0f7861ff4ff30ea9ac3154a561a7d8850a6f256a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:32:04 -0400 Subject: [PATCH 03/26] fix(review): document blank and root daily-notes folders in README and ARCHITECTURE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both docs still said an empty Obsidian folder setting falls back to "Daily Notes". A blank folder now follows Obsidian's default location for new notes (.obsidian/app.json, the vault root without it), "/" is the vault root, and daily notes in either place add no folder to the delete and move guard, the orphan exclusion, or the forward-reference counts. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 7 +++++-- README.md | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d84151d5e..dfbc7be7d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -186,7 +186,7 @@ Unlike a read that parses the block, such as `vault_read_note` with `properties_ `vault_delete_note` and `vault_move_note` refuse paths under protected folders as a server-side guardrail: -- **Default set:** the memory dir plus the daily notes folder, resolved on every delete and move as [Daily notes](#property-discovery--daily-notes) describes +- **Default set:** the memory dir plus the daily notes folder, resolved on every delete and move as [Daily notes](#property-discovery--daily-notes) describes; daily notes in the vault root or in the new-note folder through a blank setting add no folder - **Unreadable `daily-notes.json`:** a file that exists but cannot be read or parsed refuses the delete or move, because the folder to protect is unknown; setting `DAILY_NOTES_FOLDER` skips the file - **Override:** `PROTECTED_PATHS` replaces the default set entirely, and `daily-notes.json` is not read @@ -246,8 +246,11 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag **Daily notes:** `vault_get_daily_note` resolves the vault's folder and date format, each independently: `DAILY_NOTES_FOLDER`/`DAILY_NOTES_FORMAT` env setting → `.obsidian/daily-notes.json` → fallback (`Daily Notes/YYYY-MM-DD.md`). - **Read on every call:** a folder or format change in Obsidian applies to the next call, and a config file that arrives after boot needs no restart +- **Folder normalized like Obsidian:** runs of `/` or `\` become one `/` and leading and trailing slashes are dropped, so Obsidian's `"/"` is the vault root +- **Blank folder in the file:** Obsidian's new-note location (`newFileLocation: "folder"` plus `newFileFolderPath` in `.obsidian/app.json`), otherwise the vault root - **Missing file:** takes the fallbacks - **Unparseable file:** takes the fallbacks and logs a `warn` +- **Shared folder:** daily notes in the vault root, or in the new-note folder through a blank setting, add no folder to the delete and move guard, the orphan exclusion, or the forward-reference counts ### Memory @@ -314,7 +317,7 @@ Link queries use a `links` table populated during indexing: - **Orphans:** `vault_find_orphans` and `vault-orientation` resolve default exclusions on each invocation: the daily notes folder (`DAILY_NOTES_FOLDER` → `.obsidian/daily-notes.json` → `Daily Notes`), `Templates`, and the memory dir. - `ORPHAN_EXCLUDE_FOLDERS` replaces that list; a tool call's `exclude_folders` replaces it for that call. - An unreadable or malformed config logs a warning and uses `Daily Notes`; a missing file uses that fallback without a warning. - - An empty Obsidian folder setting uses the `Daily Notes` fallback. A whitespace-only folder adds no daily-folder exclusion. Neither excludes the whole vault, so root-level daily notes remain eligible. + - Daily notes in the vault root or in Obsidian's new-note folder (a blank folder setting), and a whitespace-only folder, add no daily-folder exclusion, so those daily notes remain eligible. ### Files diff --git a/README.md b/README.md index 36bd47a6e..e3f7889ba 100644 --- a/README.md +++ b/README.md @@ -421,7 +421,8 @@ When the file isn't available — or if you use the Periodic Notes plugin, whose You can set one or both — a set value always wins over the config file. Without either source, the server falls back to `Daily Notes` and `YYYY-MM-DD`. - Changes in Obsidian apply on the next call when `.obsidian/daily-notes.json` is available in the server's vault. Apply environment changes with the CLI's `restart` command, recreate the Compose container, or redeploy on your hosting platform. -- An empty Obsidian folder setting uses the server's `Daily Notes` fallback. Daily notes stored at the vault root remain eligible for orphan results; the server never excludes the whole vault automatically. +- A blank folder in Obsidian's Daily notes options means the **Default location for new notes** setting (Settings → Files and links), read from `.obsidian/app.json`. When that file isn't in the server's vault, the server uses the vault root. +- When daily notes go to the vault root, or to the new-note folder through a blank setting, the server doesn't protect them from delete and move or leave them out of orphan results: protecting or excluding that folder would cover every other note in it. > **Note:** A few date format tokens are unsupported — ordinals (`Do`, `Mo`, `DDDo`, `wo`), `dd` (2-letter weekday), `d` (weekday number), `e`, `k`/`kk`, `w` (week number), `Q` (quarter), `Z`/`ZZ` (UTC offset), and the localized formats (`L`–`LLLL`, `LT`, `LTS`). The server can't reproduce the filenames Obsidian creates with these tokens, so it could never find the notes. If your format uses any of them, `vault_get_daily_note` returns a clear error — change the format in Obsidian or set `DAILY_NOTES_FORMAT` to a supported alternative. From 4af6d6f15557164578f857de33e3289b8d3088d1 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:34:31 -0400 Subject: [PATCH 04/26] fix(daily-notes): scope the shared-folder wording to a root or blank setting, and document app.json on remote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- deploy/remote/README.md | 10 +++++++++- .../__tests__/__snapshots__/tool-surface/default.json | 8 ++++---- .../__snapshots__/tool-surface/disabled-tools.json | 8 ++++---- .../__snapshots__/tool-surface/embedding-off.json | 8 ++++---- .../tool-surface/file-tools-off+embedding-off.json | 8 ++++---- .../__snapshots__/tool-surface/file-tools-off.json | 8 ++++---- .../tool-surface/memory-off+embedding-off.json | 8 ++++---- .../memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/memory-off+file-tools-off.json | 8 ++++---- .../__snapshots__/tool-surface/memory-off.json | 8 ++++---- .../__snapshots__/tool-surface/obsidian-sync.json | 8 ++++---- .../tool-surface/readonly+embedding-off.json | 4 ++-- .../readonly+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off.json | 4 ++-- .../readonly+memory-off+embedding-off.json | 4 ++-- ...adonly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/readonly.json | 4 ++-- .../mcp-core/__tests__/tool-definitions.test.ts | 8 ++++---- src/vault-mcp/mcp-core/tools/search-tools.ts | 4 ++-- src/vault-mcp/mcp-core/tools/vault-crud-tools.ts | 2 +- src/vault-mcp/vault-operations/daily-notes.ts | 8 ++++---- src/vault-mcp/vault-operations/vault-folder-config.ts | 8 ++++---- 24 files changed, 80 insertions(+), 72 deletions(-) diff --git a/deploy/remote/README.md b/deploy/remote/README.md index d86b86bf8..428c221e9 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -550,12 +550,20 @@ that file when settings syncing is turned on in two places: Some community plugins keep API keys in their settings. Server tools never read `.obsidian/`, and the server itself opens only the two config files -above — synced settings otherwise just sit in the config volume. +above, plus `app.json` when the `app` category is synced — synced settings +otherwise just sit in the config volume. If you turn on **Core plugin settings** syncing after the server is already running, `daily-notes.json` is picked up automatically once it lands; no restart needed. +If the folder box in Obsidian's Daily notes options is empty, Obsidian puts +daily notes where **Default location for new notes** says, a setting stored +in `.obsidian/app.json`. The default `SYNC_CONFIGS` leaves that file out, so +the server then looks in the vault root. If your new notes go to a set +folder, add `app` to `SYNC_CONFIGS` and turn on **Main settings** under +Vault configuration sync, or set `DAILY_NOTES_FOLDER` as described below. + If the settings can't sync — or you use the Periodic Notes plugin, whose settings the core config file doesn't track — set `DAILY_NOTES_FOLDER` (any vault-relative path: `Journal`, `Planner/Daily`) and `DAILY_NOTES_FORMAT` 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 6ef67a370..61806dfa3 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 @@ -194,7 +194,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -394,7 +394,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 07ecc8ea6..c7b36d93f 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 96f7435b8..b5d0bf6a0 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 5256cb8e4..a4c5f6d3a 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 @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -397,7 +397,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 c696793b7..0b7c0199a 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 e2b003052..fde3e0984 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 aa7e7dafa..4474f084d 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 @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -317,7 +317,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 2ce4b76d2..07a357581 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 1b632b62a..ed2225368 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 @@ -150,7 +150,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -315,7 +315,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 c265362d8..0496106af 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 7379aac87..65f6aec79 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 ccedf2583..abf187381 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -133,7 +133,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 f534af793..d39c41a04 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 01dea62c5..f17ad9de2 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 3f954ebf2..cc3259b83 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -99,7 +99,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 32dfbde6c..d8bcc7fff 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 28bbc4886..99cef4db3 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -97,7 +97,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 216ebb27f..fdc35229a 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian's new-note folder.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -131,7 +131,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index ccbf692e4..937f67b13 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -517,7 +517,7 @@ describe("config interpolation in descriptions", () => { linePrefix: "- Links to the note", }) expect(linksEntry).toBe( - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder).", + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).", ) }) @@ -1158,7 +1158,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { }) } const LINKS_AND_PROTECTED_PATHS_ENTRY = - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)." + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)." it("describes the Deleted files outcomes when the server does not sync", () => { expect(deleteNoteOpener({})).toBe( @@ -1838,7 +1838,7 @@ describe("vault_find_orphans live folder defaults", () => { ?.split("\n") .find((line) => line.startsWith("- With exclude_folders")) expect(defaultsLine).toBe( - '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian\'s new-note folder.', + '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.', ) expect(toolConfig.description).not.toContain("Journal") expect(toolConfig.inputSchema?.exclude_folders?.description).toBe( @@ -2629,7 +2629,7 @@ describe("DISABLED_TOOLS", () => { }) } const PROTECTED_PATHS_SENTENCE = - " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when daily notes share the vault root or Obsidian's new-note folder)." + " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)." expect(linksEntry("")).toBe( `- Links to the note from other notes become broken; list them first with vault_get_backlinks.${PROTECTED_PATHS_SENTENCE}`, diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 891b0644f..7167a5a84 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -589,7 +589,7 @@ For incoming links (what links TO a note), use vault_get_backlinks. Parameters: - path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds). -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. Errors: - "path must end in …" — add the .md or .canvas extension @@ -641,7 +641,7 @@ Errors: : `the daily notes folder, "Templates", ${JSON.stringify(config.memoryDir)}` const orphanDefaultDescription = config.orphanExcludeFoldersOverride ? "With exclude_folders omitted, the server's configured list (the schema default) is used." - : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when daily notes share the vault root or Obsidian\'s new-note folder.' + : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.' // An override list may not include the daily notes folder, so the hint only // accompanies the built-in defaults. const dailyNotesFolderHint = config.orphanExcludeFoldersOverride diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 5e50638a5..6e4df10cf 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -43,7 +43,7 @@ const describeProtectedPaths = (config: VaultConfig): string => { // loadConfig strips trailing slashes from override entries. return config.protectedPathsOverride.map((protectedPath) => protectedPath + "/").join(", ") } - return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/; none when daily notes share the vault root or Obsidian's new-note folder)` + return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/; none when that setting is the vault root or blank)` } /** Sentences that point to other tools, each already "" when its tool is diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index e0c11f8be..f641f505b 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -22,8 +22,8 @@ type DailyNotesConfig = { format: string /** The folder when it holds daily notes apart from other notes, for the * delete guard, orphan exclusion and forward-reference counts. Null when - * daily notes sit in the vault root or Obsidian's new-note folder, since - * treating those as daily-only would cover every other note there too. */ + * the setting is the vault root or blank (Obsidian's new-note folder), + * since treating those as daily-only would cover every other note there. */ dedicatedFolder: string | null } @@ -110,8 +110,8 @@ const resolveDedicatedFolder = (folderSetting: DailyNotesFolderSetting): string /** Reads the daily notes folder the delete and move guard protects, from * .obsidian/daily-notes.json. - * - Returns "Daily Notes" when the file is missing, and null when daily - * notes sit in the vault root or Obsidian's new-note folder. + * - Returns "Daily Notes" when the file is missing, and null when its + * folder is the vault root or blank. * - Throws when the file exists but cannot be read or parsed. */ export const readProtectedDailyNotesFolder = async ( vaultPath: string, diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index 58932ae87..bac62d50d 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -9,9 +9,9 @@ import { readDailyNotesConfig, readProtectedDailyNotesFolder } from "./daily-not * plus the daily notes folder, resolved on each call (DAILY_NOTES_FOLDER → * .obsidian/daily-notes.json → "Daily Notes") so a folder configured only * in the vault is protected too. - * - Daily notes kept in the vault root or Obsidian's new-note folder are not - * protected, since protecting either folder would cover every other note - * in it. + * - A daily-notes.json folder that is the vault root or blank (Obsidian's + * new-note folder) protects no daily folder, since protecting either + * would cover every other note in it. * Throws when daily-notes.json exists but cannot be read, since the folder * it names is then unknown; DAILY_NOTES_FOLDER or PROTECTED_PATHS bypasses * the file. */ @@ -37,7 +37,7 @@ export const resolveEffectiveOrphanExcludeFolders = ({ }: { orphanExcludeFoldersOverride: readonly string[] | null memoryDir: string - /** Null when daily notes share the vault root or the new-note folder with other notes. */ + /** Null when the daily folder setting is the vault root or blank. */ dailyNotesFolder: string | null }): readonly string[] => { if (orphanExcludeFoldersOverride) return orphanExcludeFoldersOverride From e43f2d4ff3ce9f0b25097481d96e7b81a8956e2a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:48:02 -0400 Subject: [PATCH 05/26] style: clarify daily-notes config readers and root/blank folder docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - daily-notes.ts: accurate reader map, fallback and env-settings docs, the two folder resolvers side by side with their contrast stated, and the vault-root case made explicit. - vault-folder-config.ts: one predicate for "no daily folder", doc comments for the orphan-exclusion helpers. - vault-orientation-prompt.ts, search-queries.ts: state the null-folder contract and why the prompt reads the config once. - README, remote guide, ARCHITECTURE: one wording for root and blank settings, named files and toggles, split timing bullets; DOCKERHUB.md regenerated. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 11 +- DOCKERHUB.md | 10 +- README.md | 77 +++++----- deploy/remote/README.md | 36 +++-- .../prompts/vault-orientation-prompt.ts | 9 +- src/vault-mcp/search/search-queries.ts | 14 +- src/vault-mcp/vault-operations/daily-notes.ts | 138 +++++++++++------- .../vault-operations/vault-folder-config.ts | 20 ++- 8 files changed, 186 insertions(+), 129 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dfbc7be7d..8b54b7e8a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -186,7 +186,7 @@ Unlike a read that parses the block, such as `vault_read_note` with `properties_ `vault_delete_note` and `vault_move_note` refuse paths under protected folders as a server-side guardrail: -- **Default set:** the memory dir plus the daily notes folder, resolved on every delete and move as [Daily notes](#property-discovery--daily-notes) describes; daily notes in the vault root or in the new-note folder through a blank setting add no folder +- **Default set:** the memory dir plus the daily notes folder, resolved on every delete and move as [Daily notes](#property-discovery--daily-notes) describes; a vault-root or blank folder setting adds no folder - **Unreadable `daily-notes.json`:** a file that exists but cannot be read or parsed refuses the delete or move, because the folder to protect is unknown; setting `DAILY_NOTES_FOLDER` skips the file - **Override:** `PROTECTED_PATHS` replaces the default set entirely, and `daily-notes.json` is not read @@ -250,7 +250,7 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag - **Blank folder in the file:** Obsidian's new-note location (`newFileLocation: "folder"` plus `newFileFolderPath` in `.obsidian/app.json`), otherwise the vault root - **Missing file:** takes the fallbacks - **Unparseable file:** takes the fallbacks and logs a `warn` -- **Shared folder:** daily notes in the vault root, or in the new-note folder through a blank setting, add no folder to the delete and move guard, the orphan exclusion, or the forward-reference counts +- **Shared folder:** a vault-root or blank folder setting puts daily notes beside other notes, so it adds no folder to the delete and move guard, the orphan exclusion, or forward-reference handling (broken links into the daily folder, flagged `daily_note_forward_ref` by `vault_get_outgoing_links` and left out of `vault-orientation`'s broken-link count) ### Memory @@ -314,10 +314,10 @@ Link queries use a `links` table populated during indexing: 3. Basename (shortest-path-first for ambiguous basenames) - **Non-markdown files:** Targets that don't resolve to a note are checked against a `non_md_files` table (populated during rebuild, maintained by the file watcher). Both wikilinks and markdown-style links to `.canvas`, `.base`, images, PDFs, and other non-markdown files resolve as `kind: "file"` instead of being counted as broken. - **Outgoing links:** `vault_get_outgoing_links` returns a `kind` discriminator (`"note"` or `"file"`) plus each target's byte size (`bytes` — from the notes table for notes, from `non_md_files` for files), so clients can route notes to `vault_read_note` and files to `vault_read_file` with size awareness. -- **Orphans:** `vault_find_orphans` and `vault-orientation` resolve default exclusions on each invocation: the daily notes folder (`DAILY_NOTES_FOLDER` → `.obsidian/daily-notes.json` → `Daily Notes`), `Templates`, and the memory dir. +- **Orphans:** `vault_find_orphans` and `vault-orientation` resolve default exclusions on each invocation: the daily notes folder (resolved as [Daily notes](#property-discovery--daily-notes) describes), `Templates`, and the memory dir. - `ORPHAN_EXCLUDE_FOLDERS` replaces that list; a tool call's `exclude_folders` replaces it for that call. - An unreadable or malformed config logs a warning and uses `Daily Notes`; a missing file uses that fallback without a warning. - - Daily notes in the vault root or in Obsidian's new-note folder (a blank folder setting), and a whitespace-only folder, add no daily-folder exclusion, so those daily notes remain eligible. + - A vault-root, blank, or whitespace-only folder setting adds no daily-folder exclusion, so those daily notes remain eligible. ### Files @@ -1257,7 +1257,8 @@ Docker hardening, and durability seatbelts above. `join`, bypassing `resolveSafePath`'s hidden-path guard). - **Protected paths**: `PROTECTED_PATHS` (default: `MEMORY_DIR` plus the daily notes folder, from `DAILY_NOTES_FOLDER`, then - `.obsidian/daily-notes.json`, then `Daily Notes`) blocks deleting + `.obsidian/daily-notes.json`, then `Daily Notes`; none for a vault-root or + blank folder setting) blocks deleting notes in, moving notes out of, and moving notes into configured folders. The check (`isProtectedPath()`, shared by delete and move) runs on the canonical vault-relative path with a case-folded diff --git a/DOCKERHUB.md b/DOCKERHUB.md index 7013cd4f5..07edf1bfb 100644 --- a/DOCKERHUB.md +++ b/DOCKERHUB.md @@ -151,7 +151,7 @@ Vault Cortex indexes every [property](https://help.obsidian.md/Editing+and+forma All settings are environment variables with sensible defaults. Some defaults derive from other settings — the Default column shows each derivation, and a value you set replaces the whole derived default. Remote deployments also forward Obsidian Sync's own settings — `DEVICE_NAME`, `SYNC_MODE`, `CONFLICT_STRATEGY`, `SYNC_CONFIGS`, `SYNC_EXCLUDED_FOLDERS`, `SYNC_FILE_TYPES` — documented in the [remote guide's configuration table](https://github.com/aliasunder/vault-cortex/tree/main/deploy/remote/README.md#configuration). | Variable | Required? | Default | Description | -| --------------------------- | ----------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| --------------------------- | ----------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MCP_AUTH_TOKEN` | Yes | — | Bearer token for authentication (also the JWT signing key) | | `VAULT_PATH` | Local only | — | Host path to your vault (bind mount source; remote uses a named volume). Must not contain `*`, `?`, or `[` — rejected at startup. | | `PUBLIC_URL` | Remote only | — | Public URL for OAuth discovery metadata. Filled in automatically on Render and Railway (from `RENDER_EXTERNAL_URL` or `RAILWAY_PUBLIC_DOMAIN`) when left unset | @@ -166,10 +166,10 @@ All settings are environment variables with sensible defaults. Some defaults der | `READONLY_MODE` | — | `false` | Set `true` to hide every tool that changes the vault and skip memory folder auto-creation — connected clients can read and search but never edit. | | `DISABLED_TOOLS` | — | — | Hide individual tools by name, comma-separated (e.g. `vault_delete_note,vault_move_note`). Names match the Tool column in the [tools table](https://github.com/aliasunder/vault-cortex#tools). Subtractive only — it cannot re-enable a tool another setting hides. An unknown tool name stops the server at startup, so typos surface immediately. | | `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files | -| `PROTECTED_PATHS` | — | `MEMORY_DIR`, daily notes folder | Folders that `vault_delete_note` and `vault_move_note` refuse to touch. The default daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`). Overrides the default entirely when set. | -| `ORPHAN_EXCLUDE_FOLDERS` | — | daily notes folder, `Templates`, `MEMORY_DIR` | Folders excluded from orphan detection. `DAILY_NOTES_FOLDER` wins; otherwise `.obsidian/daily-notes.json` is reread per query (fallback `Daily Notes`). Set a list to replace all defaults, or `ORPHAN_EXCLUDE_FOLDERS=,` to exclude nothing. An empty value uses the defaults. Root-level daily notes remain eligible. See [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes). | -| `DAILY_NOTES_FOLDER` | — | from vault config | Sets the folder your daily notes live in. When unset, read from the vault's `.obsidian/daily-notes.json`, falling back to `Daily Notes`. See [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes). | -| `DAILY_NOTES_FORMAT` | — | from vault config | Sets the daily note filename format — same tokens as Obsidian's daily note date format setting. When unset, read from the vault's `.obsidian/daily-notes.json`, falling back to `YYYY-MM-DD`. See [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes). | +| `PROTECTED_PATHS` | — | `MEMORY_DIR`, daily notes folder | Folders that `vault_delete_note` and `vault_move_note` refuse to touch. The default daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`); see [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes) for root and empty settings. Overrides the default entirely when set. | +| `ORPHAN_EXCLUDE_FOLDERS` | — | daily notes folder, `Templates`, `MEMORY_DIR` | Folders excluded from orphan detection. The daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`); see [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes) for root and empty settings. Set a list to replace all defaults, or `ORPHAN_EXCLUDE_FOLDERS=,` to exclude nothing. An empty value uses the defaults. | +| `DAILY_NOTES_FOLDER` | — | from `daily-notes.json` | Sets the folder your daily notes live in. When unset, read from `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable. See [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes). | +| `DAILY_NOTES_FORMAT` | — | from `daily-notes.json` | Sets the daily note filename format — same tokens as Obsidian's daily note date format setting. When unset, read from `.obsidian/daily-notes.json`, falling back to `YYYY-MM-DD`. See [Daily notes](https://github.com/aliasunder/vault-cortex#daily-notes). | | `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution | | `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata | | `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` | diff --git a/README.md b/README.md index e3f7889ba..a74322f01 100644 --- a/README.md +++ b/README.md @@ -371,38 +371,38 @@ These are conventions, not requirements — Vault Cortex works with any property All settings are environment variables with sensible defaults. Some defaults derive from other settings — the Default column shows each derivation, and a value you set replaces the whole derived default. Remote deployments also forward Obsidian Sync's own settings — `DEVICE_NAME`, `SYNC_MODE`, `CONFLICT_STRATEGY`, `SYNC_CONFIGS`, `SYNC_EXCLUDED_FOLDERS`, `SYNC_FILE_TYPES` — documented in the [remote guide's configuration table](./deploy/remote/README.md#configuration). -| Variable | Required? | Default | Description | -| --------------------------- | ----------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MCP_AUTH_TOKEN` | Yes | — | Bearer token for authentication (also the JWT signing key) | -| `VAULT_PATH` | Local only | — | Host path to your vault (bind mount source; remote uses a named volume). Must not contain `*`, `?`, or `[` — rejected at startup. | -| `PUBLIC_URL` | Remote only | — | Public URL for OAuth discovery metadata. Filled in automatically on Render and Railway (from `RENDER_EXTERNAL_URL` or `RAILWAY_PUBLIC_DOMAIN`) when left unset | -| `OBSIDIAN_AUTH_TOKEN` | — | — | Obsidian Sync auth token. Leave empty to sign in through the `/setup` page after deploy; or the CLI's [`get-sync-token`](./cli/README.md#get-sync-token) captures it for you | -| `VAULT_NAME` | Remote only | — | Exact name of your Obsidian vault (case-sensitive) | -| `VAULT_PASSWORD` | Remote only | — | End-to-end encryption password, if your vault has one. Leave empty otherwise. | -| `STORAGE_ROOT` | — | — | One directory for everything that must persist — the vault, the search index, and Obsidian Sync state — for container hosting platforms that allow a single persistent volume (Railway, Render). Mount the volume there and set this to the same path. Must not contain `*`, `?`, or `[` — rejected at startup. | -| `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. | -| `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. | -| `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` still supplies the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS` when `false`. | -| `FILE_TOOLS_ENABLED` | — | `true` | Set `false` to hide file tools (`vault_read_file`, `vault_list_files`) — useful for remote deployments where Obsidian Sync has attachment syncing disabled. | -| `READONLY_MODE` | — | `false` | Set `true` to hide every tool that changes the vault and skip memory folder auto-creation — connected clients can read and search but never edit. | -| `DISABLED_TOOLS` | — | — | Hide individual tools by name, comma-separated (e.g. `vault_delete_note,vault_move_note`). Names match the Tool column in the [tools table](#tools). Subtractive only — it cannot re-enable a tool another setting hides. An unknown tool name stops the server at startup, so typos surface immediately. | -| `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files | -| `PROTECTED_PATHS` | — | `MEMORY_DIR`, daily notes folder | Folders that `vault_delete_note` and `vault_move_note` refuse to touch. The default daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`). Overrides the default entirely when set. | -| `ORPHAN_EXCLUDE_FOLDERS` | — | daily notes folder, `Templates`, `MEMORY_DIR` | Folders excluded from orphan detection. `DAILY_NOTES_FOLDER` wins; otherwise `.obsidian/daily-notes.json` is reread per query (fallback `Daily Notes`). Set a list to replace all defaults, or `ORPHAN_EXCLUDE_FOLDERS=,` to exclude nothing. An empty value uses the defaults. Root-level daily notes remain eligible. See [Daily notes](#daily-notes). | -| `DAILY_NOTES_FOLDER` | — | from vault config | Sets the folder your daily notes live in. When unset, read from the vault's `.obsidian/daily-notes.json`, falling back to `Daily Notes`. See [Daily notes](#daily-notes). | -| `DAILY_NOTES_FORMAT` | — | from vault config | Sets the daily note filename format — same tokens as Obsidian's daily note date format setting. When unset, read from the vault's `.obsidian/daily-notes.json`, falling back to `YYYY-MM-DD`. See [Daily notes](#daily-notes). | -| `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution | -| `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata | -| `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` | -| `LOG_DIR` | — | `/data/logs` (remote), `$STORAGE_ROOT/data/logs` (single-volume), `none` (local) | Directory for log files that survive container re-creation. The container's own log (what `docker logs` shows) is always written, but Docker discards it whenever the container is recreated — on image updates or config changes. Date-stamped files under `LOG_DIR` live on the data volume and survive. `none` keeps only the container log. | -| `LOG_RETENTION_DAYS` | — | `90` | Days to keep log files before automatic cleanup on startup; only applies when `LOG_DIR` is a path | -| `WINDOWS_MODE` | — | `false` | On Windows? Set `true`. Switches the file watcher to polling and note moves to rename-based writes so a vault on a `C:` drive works through Docker Desktop. Safe to leave on for any Windows setup; unneeded on macOS/Linux/WSL2. | -| `MAX_FILE_BYTES` | — | `52428800` (50 MiB) | Maximum file size `vault_read_file` will read (in bytes). Files exceeding this are rejected before reading. Raise for vaults with very large individual files. | -| `MAX_IMAGE_OUTPUT_BYTES` | — | `49152` (48 KiB) | Byte budget for images delivered by `vault_read_file`, in binary bytes before base64 encoding. Images exceeding this are downscaled and recompressed to fit. Sized for the tightest mainstream MCP client cap; raise for clients that accept larger responses. | -| `MAX_PDF_RENDER_PAGES` | — | `5` | Maximum PDF pages to render as images when `raw: true` is set on `vault_read_file`. The per-page byte budget is `MAX_IMAGE_OUTPUT_BYTES` divided evenly across the rendered pages — fewer pages means higher quality each. | -| `TRASH_RETENTION_DAYS` | Local only | `30` | Days a note deleted under Obsidian's default "Move to system trash" setting stays in `.trash/` before the server cleans it up. Set `none` to keep those notes forever. Only notes the server itself moved there are cleaned up. With Obsidian Sync, deletes are permanent on the server and recoverable from Sync's version history. | -| `TRUST_PROXY_HOPS` | — | `0` | Number of trusted reverse-proxy hops used to derive the client IP from `X-Forwarded-For` (OAuth rate limiting, request logs). Set `1` when exactly one proxy you control sits in front of the server (Caddy, nginx, Cloudflare Tunnel, API Gateway). With `0`, injected forwarding headers are ignored. | -| `TRUST_FORWARDED_HOPS` | — | `0` | How many trailing `for=` entries in the [RFC 7239](https://www.rfc-editor.org/rfc/rfc7239) `Forwarded` header belong to proxies you control. `0` ignores the header; `1` when the proxy in front writes it (e.g. AWS API Gateway); `2` when a CDN fronts that proxy and is the only way to reach it. | +| Variable | Required? | Default | Description | +| --------------------------- | ----------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_AUTH_TOKEN` | Yes | — | Bearer token for authentication (also the JWT signing key) | +| `VAULT_PATH` | Local only | — | Host path to your vault (bind mount source; remote uses a named volume). Must not contain `*`, `?`, or `[` — rejected at startup. | +| `PUBLIC_URL` | Remote only | — | Public URL for OAuth discovery metadata. Filled in automatically on Render and Railway (from `RENDER_EXTERNAL_URL` or `RAILWAY_PUBLIC_DOMAIN`) when left unset | +| `OBSIDIAN_AUTH_TOKEN` | — | — | Obsidian Sync auth token. Leave empty to sign in through the `/setup` page after deploy; or the CLI's [`get-sync-token`](./cli/README.md#get-sync-token) captures it for you | +| `VAULT_NAME` | Remote only | — | Exact name of your Obsidian vault (case-sensitive) | +| `VAULT_PASSWORD` | Remote only | — | End-to-end encryption password, if your vault has one. Leave empty otherwise. | +| `STORAGE_ROOT` | — | — | One directory for everything that must persist — the vault, the search index, and Obsidian Sync state — for container hosting platforms that allow a single persistent volume (Railway, Render). Mount the volume there and set this to the same path. Must not contain `*`, `?`, or `[` — rejected at startup. | +| `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. | +| `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. | +| `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` still supplies the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS` when `false`. | +| `FILE_TOOLS_ENABLED` | — | `true` | Set `false` to hide file tools (`vault_read_file`, `vault_list_files`) — useful for remote deployments where Obsidian Sync has attachment syncing disabled. | +| `READONLY_MODE` | — | `false` | Set `true` to hide every tool that changes the vault and skip memory folder auto-creation — connected clients can read and search but never edit. | +| `DISABLED_TOOLS` | — | — | Hide individual tools by name, comma-separated (e.g. `vault_delete_note,vault_move_note`). Names match the Tool column in the [tools table](#tools). Subtractive only — it cannot re-enable a tool another setting hides. An unknown tool name stops the server at startup, so typos surface immediately. | +| `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files | +| `PROTECTED_PATHS` | — | `MEMORY_DIR`, daily notes folder | Folders that `vault_delete_note` and `vault_move_note` refuse to touch. The default daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`); see [Daily notes](#daily-notes) for root and empty settings. Overrides the default entirely when set. | +| `ORPHAN_EXCLUDE_FOLDERS` | — | daily notes folder, `Templates`, `MEMORY_DIR` | Folders excluded from orphan detection. The daily notes folder is read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json` (default `Daily Notes`); see [Daily notes](#daily-notes) for root and empty settings. Set a list to replace all defaults, or `ORPHAN_EXCLUDE_FOLDERS=,` to exclude nothing. An empty value uses the defaults. | +| `DAILY_NOTES_FOLDER` | — | from `daily-notes.json` | Sets the folder your daily notes live in. When unset, read from `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable. See [Daily notes](#daily-notes). | +| `DAILY_NOTES_FORMAT` | — | from `daily-notes.json` | Sets the daily note filename format — same tokens as Obsidian's daily note date format setting. When unset, read from `.obsidian/daily-notes.json`, falling back to `YYYY-MM-DD`. See [Daily notes](#daily-notes). | +| `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution | +| `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata | +| `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` | +| `LOG_DIR` | — | `/data/logs` (remote), `$STORAGE_ROOT/data/logs` (single-volume), `none` (local) | Directory for log files that survive container re-creation. The container's own log (what `docker logs` shows) is always written, but Docker discards it whenever the container is recreated — on image updates or config changes. Date-stamped files under `LOG_DIR` live on the data volume and survive. `none` keeps only the container log. | +| `LOG_RETENTION_DAYS` | — | `90` | Days to keep log files before automatic cleanup on startup; only applies when `LOG_DIR` is a path | +| `WINDOWS_MODE` | — | `false` | On Windows? Set `true`. Switches the file watcher to polling and note moves to rename-based writes so a vault on a `C:` drive works through Docker Desktop. Safe to leave on for any Windows setup; unneeded on macOS/Linux/WSL2. | +| `MAX_FILE_BYTES` | — | `52428800` (50 MiB) | Maximum file size `vault_read_file` will read (in bytes). Files exceeding this are rejected before reading. Raise for vaults with very large individual files. | +| `MAX_IMAGE_OUTPUT_BYTES` | — | `49152` (48 KiB) | Byte budget for images delivered by `vault_read_file`, in binary bytes before base64 encoding. Images exceeding this are downscaled and recompressed to fit. Sized for the tightest mainstream MCP client cap; raise for clients that accept larger responses. | +| `MAX_PDF_RENDER_PAGES` | — | `5` | Maximum PDF pages to render as images when `raw: true` is set on `vault_read_file`. The per-page byte budget is `MAX_IMAGE_OUTPUT_BYTES` divided evenly across the rendered pages — fewer pages means higher quality each. | +| `TRASH_RETENTION_DAYS` | Local only | `30` | Days a note deleted under Obsidian's default "Move to system trash" setting stays in `.trash/` before the server cleans it up. Set `none` to keep those notes forever. Only notes the server itself moved there are cleaned up. With Obsidian Sync, deletes are permanent on the server and recoverable from Sync's version history. | +| `TRUST_PROXY_HOPS` | — | `0` | Number of trusted reverse-proxy hops used to derive the client IP from `X-Forwarded-For` (OAuth rate limiting, request logs). Set `1` when exactly one proxy you control sits in front of the server (Caddy, nginx, Cloudflare Tunnel, API Gateway). With `0`, injected forwarding headers are ignored. | +| `TRUST_FORWARDED_HOPS` | — | `0` | How many trailing `for=` entries in the [RFC 7239](https://www.rfc-editor.org/rfc/rfc7239) `Forwarded` header belong to proxies you control. `0` ignores the header; `1` when the proxy in front writes it (e.g. AWS API Gateway); `2` when a CDN fronts that proxy and is the only way to reach it. | See [`templates/memory/`](./templates/memory/) for memory file examples and the dated-entry design philosophy. @@ -411,18 +411,19 @@ See [`templates/memory/`](./templates/memory/) for memory file examples and the `vault_get_daily_note` and the daily-review prompt find your daily notes using the folder and filename date format configured in Obsidian, read from your vault's `.obsidian/daily-notes.json`: - **Local mode** reads the file straight from your bind-mounted vault — nothing to set up. -- **Remote mode** receives it through Obsidian Sync's vault configuration syncing. The server pulls it by default (the `SYNC_CONFIGS` setting in `.env`), but you'll likely need to enable the push side: Obsidian Settings → Sync → **Vault configuration sync**, per device. Details: the [remote guide's Daily notes section](./deploy/remote/README.md#daily-notes). +- **Remote mode** receives it through Obsidian Sync's vault configuration syncing. The server pulls it by default (the `SYNC_CONFIGS` setting in `.env`), but your devices must also send it: turn on Obsidian Settings → Sync → **Vault configuration sync** on each device. Details: the [remote guide's Daily notes section](./deploy/remote/README.md#daily-notes). -When the file isn't available — or if you use the Periodic Notes plugin, whose settings it doesn't reflect — set the values yourself: +When the file isn't available — or if you use the Periodic Notes plugin, whose settings `daily-notes.json` doesn't hold — set the values yourself: - `DAILY_NOTES_FOLDER` — any vault-relative path: `Journal`, `Planner/Daily` - `DAILY_NOTES_FORMAT` — same tokens as Obsidian's date format setting: `YYYY-MM-DD-dddd`, `YYYY/MM/DD`, `MMM D, YYYY`, … -You can set one or both — a set value always wins over the config file. Without either source, the server falls back to `Daily Notes` and `YYYY-MM-DD`. +You can set one or both — a set value always wins over the config file. A value you leave unset comes from `daily-notes.json`; when that file is missing, the server uses `Daily Notes` and `YYYY-MM-DD`. -- Changes in Obsidian apply on the next call when `.obsidian/daily-notes.json` is available in the server's vault. Apply environment changes with the CLI's `restart` command, recreate the Compose container, or redeploy on your hosting platform. -- A blank folder in Obsidian's Daily notes options means the **Default location for new notes** setting (Settings → Files and links), read from `.obsidian/app.json`. When that file isn't in the server's vault, the server uses the vault root. -- When daily notes go to the vault root, or to the new-note folder through a blank setting, the server doesn't protect them from delete and move or leave them out of orphan results: protecting or excluding that folder would cover every other note in it. +- Changes in Obsidian apply on the next call when `.obsidian/daily-notes.json` is available in the server's vault. +- Apply environment changes with the CLI's `restart` command, recreate the Compose container, or redeploy on your hosting platform. +- If you leave the folder empty in Obsidian's Daily notes options, the server follows Obsidian's **Default location for new notes** setting (Settings → Files and links), read from `.obsidian/app.json`. **Same folder as current file** counts as the vault root, since Obsidian creates a daily note with no current file to place it beside. When `app.json` isn't in the server's vault, as in remote mode with the default `SYNC_CONFIGS`, the server uses the vault root. +- If your daily notes live in the vault root, or in the new-note folder because the folder setting is empty, delete and move don't protect them and orphan results can include them. Protecting or excluding that folder would also cover every other note in it. > **Note:** A few date format tokens are unsupported — ordinals (`Do`, `Mo`, `DDDo`, `wo`), `dd` (2-letter weekday), `d` (weekday number), `e`, `k`/`kk`, `w` (week number), `Q` (quarter), `Z`/`ZZ` (UTC offset), and the localized formats (`L`–`LLLL`, `LT`, `LTS`). The server can't reproduce the filenames Obsidian creates with these tokens, so it could never find the notes. If your format uses any of them, `vault_get_daily_note` returns a clear error — change the format in Obsidian or set `DAILY_NOTES_FORMAT` to a supported alternative. diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 428c221e9..26c6bf282 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -540,18 +540,19 @@ that file when settings syncing is turned on in two places: - **Server side** — `SYNC_CONFIGS` defaults to `core-plugin-data,community-plugin-data`, the two categories the server - reads: daily notes settings and community plugin settings (e.g. the Tasks - plugin's format). It takes a comma-separated list of Obsidian's settings - categories (`app`, `appearance`, `appearance-data`, `hotkey`, - `core-plugin`, `core-plugin-data`, `community-plugin`, + reads: daily notes settings (Obsidian's **Core plugin settings** toggle) + and community plugin settings, such as the Tasks plugin's format + (**Installed community plugins**). It takes a comma-separated list of + Obsidian's settings categories (`app`, `appearance`, `appearance-data`, + `hotkey`, `core-plugin`, `core-plugin-data`, `community-plugin`, `community-plugin-data`); set `SYNC_CONFIGS=none` in `.env` to disable. - **Desktop side** — Obsidian Settings → Sync → **Vault configuration sync**, enabled per device. Obsidian only pushes categories you enable there. Some community plugins keep API keys in their settings. Server tools never -read `.obsidian/`, and the server itself opens only the two config files -above, plus `app.json` when the `app` category is synced — synced settings -otherwise just sit in the config volume. +read `.obsidian/`, and the server itself opens only `daily-notes.json`, the +Tasks plugin's settings, and `app.json` when the `app` category is synced — +other synced settings just sit in the vault's `.obsidian/` folder. If you turn on **Core plugin settings** syncing after the server is already running, `daily-notes.json` is picked up automatically once it @@ -560,16 +561,27 @@ lands; no restart needed. If the folder box in Obsidian's Daily notes options is empty, Obsidian puts daily notes where **Default location for new notes** says, a setting stored in `.obsidian/app.json`. The default `SYNC_CONFIGS` leaves that file out, so -the server then looks in the vault root. If your new notes go to a set -folder, add `app` to `SYNC_CONFIGS` and turn on **Main settings** under -Vault configuration sync, or set `DAILY_NOTES_FOLDER` as described below. +the server then looks in the vault root. **Same folder as current file** also +means the vault root, as it does in Obsidian for daily notes. If your new +notes go to a set folder, either: + +- add `app` to `SYNC_CONFIGS` and turn on **Main settings** under Vault + configuration sync — a `.env` edit, so re-create the container as described + below; or +- set `DAILY_NOTES_FOLDER`, as described below. + +Daily notes in the vault root, or in the new-note folder because the folder +box is empty, aren't protected from delete and move and can appear in orphan +results; the [main README's Daily notes section](../../README.md#daily-notes) +explains why. If the settings can't sync — or you use the Periodic Notes plugin, whose settings the core config file doesn't track — set `DAILY_NOTES_FOLDER` (any vault-relative path: `Journal`, `Planner/Daily`) and `DAILY_NOTES_FORMAT` (same tokens as Obsidian's date format setting) in `.env`. You can set one -or both — a set value always wins over the config file. Without either -source, the server falls back to the `Daily Notes` folder and `YYYY-MM-DD`. +or both — a set value always wins over the config file. A value you leave +unset while `daily-notes.json` hasn't reached the server comes from the +fallbacks: the `Daily Notes` folder and `YYYY-MM-DD`. `.env` edits take effect when the container is re-created: set up with the CLI, run `npx vault-cortex@latest restart`; with Compose, `docker compose up -d`; diff --git a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts index f08ede275..025001db1 100644 --- a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts @@ -104,9 +104,12 @@ const formatExcludedForwardRefs = (params: { }): string => { const { excludedFolder, excludedCount } = params - if (excludedCount === 0) return "" + // brokenLinkCount excludes nothing without a folder, so a null folder + // always comes with a zero count. + if (excludedFolder === null || excludedCount === 0) return "" - // Collapses a trailing slash the folder name may already carry + // brokenLinkCount echoes the folder as its caller passed it, which may end + // in a slash; the label shows exactly one. const excludedFolderLabel = `${excludedFolder}/`.replace(/\/+$/, "/") const forwardReferenceLabel = excludedCount === 1 ? "forward-ref" : "forward-refs" return `excludes ${excludedCount} ${forwardReferenceLabel} in ${excludedFolderLabel}` @@ -173,6 +176,8 @@ export const registerVaultOrientationPrompt = ({ const memoryFiles = memoryStore ? await memoryStore.listMemoryFiles({ vaultPath }, reqLogger) : [] + // Read once rather than through readEffectiveOrphanExcludeFolders, + // because the broken-link count below needs the same daily folder. const dailyNotesConfig = await readDailyNotesConfig( { vaultPath, diff --git a/src/vault-mcp/search/search-queries.ts b/src/vault-mcp/search/search-queries.ts index 9423a409d..ff241efd6 100644 --- a/src/vault-mcp/search/search-queries.ts +++ b/src/vault-mcp/search/search-queries.ts @@ -1462,9 +1462,10 @@ export const getBacklinks = ( * "file" for resolved non-markdown files (.canvas, .base, images, etc.), * defaulting to "note" for unresolved (broken) links. Accepts both * .md and .canvas paths — canvas file-node references appear as outgoing. - * When the caller passes the vault's daily notes folder (resolved fresh - * via readDailyNotesConfig), broken links under it are flagged - * daily_note_forward_ref — expected "create on click" navigation. */ + * When the caller passes the vault's daily notes folder (readDailyNotesConfig's + * dedicatedFolder), broken links under it are flagged + * daily_note_forward_ref — expected "create on click" navigation. Null, for a + * vault-root or blank setting, flags none. */ export const getOutgoingLinks = ( context: SearchQueryContext, params: { path: string; dailyNotesFolder?: string | null }, @@ -1585,10 +1586,11 @@ type BrokenLinkResult = { /** Counts unique broken link targets — links whose targets exist in * neither the notes table nor the non_md_files table. When the caller - * passes the vault's daily notes folder (resolved fresh via - * readDailyNotesConfig), broken links under it are excluded — they are + * passes the vault's daily notes folder (readDailyNotesConfig's + * dedicatedFolder), broken links under it are excluded — they are * forward-references (intentional "create on click" navigation), not - * genuinely broken. Returns the count plus exclusion metadata so + * genuinely broken. Null, for a vault-root or blank setting, excludes none. + * Returns the count plus exclusion metadata so * callers can communicate what was filtered. */ export const brokenLinkCount = ( context: SearchQueryContext, diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index f641f505b..ef0a89606 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -10,10 +10,13 @@ import { isRecord } from "../../utils/is-record.js" // ── Config reading ────────────────────────────────────────────── // -// Three readers, each wrapping readDailyNotesFile: -// readProtectedDailyNotesFolder — strict: throws on an unreadable file (for the delete/move guard) +// File readers: +// readDailyNotesFile — parses .obsidian/daily-notes.json; throws on an unreadable file +// readNewNoteFolder — Obsidian's new-note folder from .obsidian/app.json; warns + falls back +// Their callers: +// readProtectedDailyNotesFolder — strict: rethrows an unreadable daily-notes.json (for the delete/move guard) // readDailyNotesFileOrFallback — tolerant: warns + falls back (for reads and prompts) -// readDailyNotesConfig — public: env → file → fallbacks, uses the tolerant reader +// readDailyNotesConfig — public: env → file → fallbacks, through the tolerant reader type DailyNotesConfig = { /** Where daily notes are read from: vault-relative, with no leading or @@ -28,7 +31,8 @@ type DailyNotesConfig = { } /** The folder setting in daily-notes.json: - * - `no-file`: the file does not exist. + * - `no-file`: no usable file. It does not exist, or the tolerant reader + * could not read or parse it. * - `blank`: the file exists with a missing or empty folder, so Obsidian * puts daily notes in its location for new notes. * - `named`: the normalized folder; "" is the vault root. */ @@ -41,19 +45,23 @@ type DailyNotesFileSettings = { } /** Per-field settings from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env - * vars; each set field takes precedence over .obsidian/daily-notes.json. */ + * vars; each set field takes precedence over .obsidian/daily-notes.json. + * loadConfig never yields a blank folder, so `folder: ""` arrives only from + * a resolved DailyNotesConfig passed back in, and means the vault root. */ export type DailyNotesEnvSettings = { folder?: string | undefined format?: string | undefined } +// The server's own choice for a vault with no daily-notes.json. Obsidian then +// uses its location for new notes, which is not a sensible folder to assume. export const DEFAULT_DAILY_NOTES_FOLDER = "Daily Notes" +// Obsidian's own default format. const DEFAULT_DAILY_NOTES_FORMAT = "YYYY-MM-DD" -// The format matches Obsidian's default; the folder is this server's own -// choice — Obsidian with no daily-notes.json creates dailies in its location -// for new notes, which is not a sensible folder for the server to assume. +/** The settings when daily-notes.json is missing, and when the tolerant + * reader cannot read it. */ const FALLBACK_FILE_SETTINGS: DailyNotesFileSettings = { folderSetting: { kind: "no-file" }, format: DEFAULT_DAILY_NOTES_FORMAT, @@ -75,7 +83,9 @@ const normalizeFolderSetting = (folderSetting: string): string => { const readDailyNotesFile = async (vaultPath: string): Promise => { try { // Read on every call, so a folder or format change in Obsidian applies - // to the next operation rather than after a restart. + // to the next operation rather than after a restart. readFile, not + // readFileOrNull: that helper returns null for a folder at this path, + // which the delete guard must refuse as unreadable, not treat as missing. const configFileContent = await readFile( join(vaultPath, ".obsidian", "daily-notes.json"), "utf8", @@ -83,6 +93,8 @@ const readDailyNotesFile = async (vaultPath: string): Promise 0 ? { kind: "named", folder: normalizeFolderSetting(settings.folder) } @@ -102,10 +114,60 @@ const readDailyNotesFile = async (vaultPath: string): Promise => { + try { + const appConfigContent = await readFileOrNull(join(vaultPath, ".obsidian", "app.json")) + + if (appConfigContent === null) return "" + + const parsedConfig: unknown = JSON.parse(appConfigContent) + const settings = isRecord(parsedConfig) ? parsedConfig : {} + const { newFileLocation, newFileFolderPath } = settings + + if (newFileLocation === "folder" && typeof newFileFolderPath === "string") { + return normalizeFolderSetting(newFileFolderPath) + } + return "" + } catch (error) { + // Reads and prompts only, so a wrong folder costs a missed note. + logger.warn("cannot read new note location, using the vault root", { + error: describeError(error), + }) + return "" + } +} + +/** The folder daily notes are read from; a blank setting follows Obsidian to + * its new-note folder. */ +const resolveDailyNotesFolder = async ( + { folderSetting, vaultPath }: { folderSetting: DailyNotesFolderSetting; vaultPath: string }, + logger: Logger, +): Promise => { + if (folderSetting.kind === "no-file") return DEFAULT_DAILY_NOTES_FOLDER + if (folderSetting.kind === "blank") return readNewNoteFolder(vaultPath, logger) + return folderSetting.folder +} + +/** The folder that holds daily notes apart from other notes, or null. Unlike + * resolveDailyNotesFolder, a blank or vault-root setting gives null, because + * that folder also holds other new notes. */ const resolveDedicatedFolder = (folderSetting: DailyNotesFolderSetting): string | null => { if (folderSetting.kind === "no-file") return DEFAULT_DAILY_NOTES_FOLDER if (folderSetting.kind === "blank") return null - return folderSetting.folder || null + + // A vault-root setting ("/") normalizes to "". + const isVaultRoot = folderSetting.folder === "" + + if (isVaultRoot) return null + return folderSetting.folder } /** Reads the daily notes folder the delete and move guard protects, from @@ -148,46 +210,6 @@ const readDailyNotesFileOrFallback = async ( } } -/** Obsidian's location for new notes, read from `newFileLocation` and - * `newFileFolderPath` in .obsidian/app.json. - * - Returns the folder when new notes go to a set folder, else the vault - * root (""). "Same folder as current file" also means the root, since a - * daily note has no current file. - * - A missing or unreadable app.json means Obsidian's default, the root. - * - Unlike Obsidian, a set folder that no longer exists is not swapped for - * the root; the daily note then reads as missing. */ -const readNewNoteFolder = async (vaultPath: string, logger: Logger): Promise => { - try { - const appConfigContent = await readFileOrNull(join(vaultPath, ".obsidian", "app.json")) - - if (appConfigContent === null) return "" - - const parsedConfig: unknown = JSON.parse(appConfigContent) - const settings = isRecord(parsedConfig) ? parsedConfig : {} - const { newFileLocation, newFileFolderPath } = settings - - if (newFileLocation === "folder" && typeof newFileFolderPath === "string") { - return normalizeFolderSetting(newFileFolderPath) - } - return "" - } catch (error) { - // Reads and prompts only, so a wrong folder costs a missed note. - logger.warn("cannot read new note location, using the vault root", { - error: describeError(error), - }) - return "" - } -} - -const resolveDailyNotesFolder = async ( - { folderSetting, vaultPath }: { folderSetting: DailyNotesFolderSetting; vaultPath: string }, - logger: Logger, -): Promise => { - if (folderSetting.kind === "no-file") return DEFAULT_DAILY_NOTES_FOLDER - if (folderSetting.kind === "blank") return readNewNoteFolder(vaultPath, logger) - return folderSetting.folder -} - /** Resolves the vault's daily note folder and filename format with * per-field precedence: env setting → .obsidian/daily-notes.json → * the fallbacks ("Daily Notes", "YYYY-MM-DD"). @@ -204,21 +226,25 @@ export const readDailyNotesConfig = async ( logger: Logger, ): Promise => { const { vaultPath, envSettings } = params + const envFormat = envSettings?.format + + // "" is a set folder here, the vault root (see DailyNotesEnvSettings), so + // this checks for undefined; `|| null` below then marks the root as shared. const envFolder = envSettings?.folder + const hasEnvFolder = envFolder !== undefined - // Both fields set — the files can't contribute anything, skip I/O. A - // resolved config passed back in can carry "" (the vault root) as its folder. - if (envFolder !== undefined && envSettings?.format) { - return { folder: envFolder, format: envSettings.format, dedicatedFolder: envFolder || null } + // Both fields set — the files can't contribute anything, skip I/O. + if (hasEnvFolder && envFormat) { + return { folder: envFolder, format: envFormat, dedicatedFolder: envFolder || null } } const { folderSetting, format: fileFormat } = await readDailyNotesFileOrFallback( vaultPath, logger, ) - const format = envSettings?.format ?? fileFormat + const format = envFormat ?? fileFormat - if (envFolder !== undefined) { + if (hasEnvFolder) { return { folder: envFolder, format, dedicatedFolder: envFolder || null } } diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index bac62d50d..16aad4138 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -2,6 +2,12 @@ import type { VaultConfig } from "../config.js" import type { Logger } from "../../logger.js" import { readDailyNotesConfig, readProtectedDailyNotesFolder } from "./daily-notes.js" +/** False for null (a vault-root or blank setting) and for a whitespace-only + * setting: neither names a daily folder to add. */ +const isDailyFolderSet = (dailyFolder: string | null): dailyFolder is string => { + return Boolean(dailyFolder?.trim()) +} + /** The folders that delete and move refuse to touch. * - PROTECTED_PATHS, when set, replaces the defaults entirely, so only the * folders it lists are protected. @@ -26,10 +32,11 @@ export const resolveEffectiveProtectedPaths = async ( const dailyFolder = await readProtectedDailyNotesFolder(vaultPath, logger) - // A whitespace-only folder in daily-notes.json protects nothing. - return dailyFolder?.trim() ? [config.memoryDir, dailyFolder] : [config.memoryDir] + return isDailyFolderSet(dailyFolder) ? [config.memoryDir, dailyFolder] : [config.memoryDir] } +/** The folders orphan results leave out: ORPHAN_EXCLUDE_FOLDERS when set, + * otherwise the daily notes folder, "Templates", and the memory dir. */ export const resolveEffectiveOrphanExcludeFolders = ({ orphanExcludeFoldersOverride, memoryDir, @@ -42,19 +49,22 @@ export const resolveEffectiveOrphanExcludeFolders = ({ }): readonly string[] => { if (orphanExcludeFoldersOverride) return orphanExcludeFoldersOverride - /** Whitespace-only settings exclude no daily folder; spaces in a nonblank folder name stay. */ - return dailyNotesFolder?.trim() + return isDailyFolderSet(dailyNotesFolder) ? [dailyNotesFolder, "Templates", memoryDir] : ["Templates", memoryDir] } -/** Only the daily folder affects orphan exclusions, so its env override skips file I/O. */ +/** resolveEffectiveOrphanExcludeFolders for callers holding only the config: + * reads .obsidian/daily-notes.json unless ORPHAN_EXCLUDE_FOLDERS or + * DAILY_NOTES_FOLDER already settles the list. */ export const readEffectiveOrphanExcludeFolders = async ( { config, vaultPath }: { config: VaultConfig; vaultPath: string }, logger: Logger, ): Promise => { if (config.orphanExcludeFoldersOverride) return config.orphanExcludeFoldersOverride + // The daily folder is the only default that varies, so an env value for + // it means no file read. if (config.dailyNotesFolder) { return resolveEffectiveOrphanExcludeFolders({ orphanExcludeFoldersOverride: null, From 549e7dce77f116581d9186fca17fed7b8edaf8f2 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:49:24 -0400 Subject: [PATCH 06/26] fix(daily-notes): say the orphan-hint folder differs when the daily setting is blank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../mcp-core/__tests__/__snapshots__/tool-surface/default.json | 2 +- .../__tests__/__snapshots__/tool-surface/disabled-tools.json | 2 +- .../__tests__/__snapshots__/tool-surface/embedding-off.json | 2 +- .../tool-surface/file-tools-off+embedding-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/file-tools-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+embedding-off.json | 2 +- .../tool-surface/memory-off+file-tools-off+embedding-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+file-tools-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/memory-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/obsidian-sync.json | 2 +- .../__snapshots__/tool-surface/readonly+embedding-off.json | 2 +- .../tool-surface/readonly+file-tools-off+embedding-off.json | 2 +- .../__snapshots__/tool-surface/readonly+file-tools-off.json | 2 +- .../tool-surface/readonly+memory-off+embedding-off.json | 2 +- .../readonly+memory-off+file-tools-off+embedding-off.json | 2 +- .../tool-surface/readonly+memory-off+file-tools-off.json | 2 +- .../__snapshots__/tool-surface/readonly+memory-off.json | 2 +- .../mcp-core/__tests__/__snapshots__/tool-surface/readonly.json | 2 +- src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts | 2 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 61806dfa3..459d77f7b 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 @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 c7b36d93f..83b19d9a6 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 b5d0bf6a0..1364e5fee 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 a4c5f6d3a..58e569812 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 @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 0b7c0199a..cd152d5b4 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 fde3e0984..7fda0f9b4 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 @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 4474f084d..77578005a 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 @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 07a357581..52f24d150 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 @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 ed2225368..65b3c4462 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 @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 0496106af..ad2115dc1 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 65f6aec79..175063578 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 abf187381..997ea3a9d 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 d39c41a04..e361623e9 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 f17ad9de2..179828a7c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 cc3259b83..b068d5361 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 d8bcc7fff..263389a7e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 99cef4db3..79967d02e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { 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 fdc35229a..d4a9b6886 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 937f67b13..bb391e467 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -2653,7 +2653,7 @@ describe("DISABLED_TOOLS", () => { '. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder)${ENTRY_END}`, + `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank)${ENTRY_END}`, ) expect(excludeFoldersEntry("vault_get_daily_note")).toBe(`${ENTRY_START}${ENTRY_END}`) }) diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 7167a5a84..69ba8fc26 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -648,7 +648,7 @@ Errors: ? "" : whenToolEnabledText( "vault_get_daily_note", - " (vault_get_daily_note's path starts with the daily notes folder)", + " (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank)", ) registerTool( From e55a42a8eb9a781e144303d029e13f490b451c96 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:55:57 -0400 Subject: [PATCH 07/26] test(daily-notes): cover blank-folder resolution in the prompts and outgoing links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Root results from readDailyNotesConfig now assert no warn, since the unreadable-app.json fallback also returns the vault root. - app.json with a non-string or missing folder path, a null body, or a directory at its path resolves to the root without a warn. - vault_get_outgoing_links, daily-review and vault-orientation treat the new-note folder a blank daily setting resolves to as shared: no forward-reference flags, no orphan exclusion, no forward-ref count. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/tool-definitions.test.ts | 59 +++++++++++++++++++ .../__tests__/daily-review-prompt.test.ts | 38 ++++++++++++ .../vault-orientation-prompt.test.ts | 20 +++++++ .../__tests__/daily-notes.test.ts | 43 ++++++++++++++ 4 files changed, 160 insertions(+) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index bb391e467..602539475 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -1874,6 +1874,65 @@ describe("vault_find_orphans live folder defaults", () => { }) }) +describe("vault_get_outgoing_links daily forward references", () => { + // Obsidian puts daily notes in the new-note folder (Inbox here) when the + // daily-notes.json folder is blank; that folder also holds other notes. + const NEW_NOTE_LOCATION = '{"newFileLocation":"folder","newFileFolderPath":"Inbox"}' + + it.each([ + { label: "a folder set to Inbox", settings: '{"folder":"Inbox"}', isForwardRef: true }, + { + label: "a blank folder that resolves to Inbox", + settings: '{"folder":""}', + isForwardRef: false, + }, + ])( + "sets daily_note_forward_ref to $isForwardRef on a missing Inbox note for $label", + async ({ settings, isForwardRef }) => { + const vaultPath = await mkdtemp(join(tmpdir(), "outgoing-links-handler-")) + onTestFinished(() => rm(vaultPath, { recursive: true, force: true })) + await mkdir(join(vaultPath, ".obsidian")) + await writeFile(join(vaultPath, ".obsidian/daily-notes.json"), settings) + await writeFile(join(vaultPath, ".obsidian/app.json"), NEW_NOTE_LOCATION) + const search = createSearchIndex(":memory:") + search.upsertNote( + { + filePath: "Inbox/2026-06-16.md", + rawContent: "[[Inbox/2026-06-17]]\n", + fileStat: { mtimeMs: 1000, size: 21 }, + }, + logger, + ) + const outgoingLinksCall = registerWithConfig({}, { vaultPath, search }).find( + ([name]) => name === TOOL_NAMES.VAULT_GET_OUTGOING_LINKS, + ) + + if (!outgoingLinksCall) throw new Error("vault_get_outgoing_links not registered") + + const result = z + .object({ content: z.array(z.object({ text: z.string() })) }) + .parse( + await outgoingLinksCall[2]({ path: "Inbox/2026-06-16.md" }, { requestId: "links-1" }), + ) + + expect(JSON.parse(requireTextContent(result))).toEqual({ + path: "Inbox/2026-06-16.md", + outgoing_links: [ + { + path: "Inbox/2026-06-17", + title: null, + exists: false, + kind: "note", + bytes: null, + daily_note_forward_ref: isForwardRef, + }, + ], + count: 1, + }) + }, + ) +}) + describe("vault_list_tasks handler", () => { const mockExtra = { requestId: "test-1", sessionId: "session-1" } diff --git a/src/vault-mcp/mcp-core/prompts/__tests__/daily-review-prompt.test.ts b/src/vault-mcp/mcp-core/prompts/__tests__/daily-review-prompt.test.ts index 8b79e3f33..935e3cabf 100644 --- a/src/vault-mcp/mcp-core/prompts/__tests__/daily-review-prompt.test.ts +++ b/src/vault-mcp/mcp-core/prompts/__tests__/daily-review-prompt.test.ts @@ -433,6 +433,44 @@ describe("daily-review handler", () => { ) }) + it("reads a blank-folder daily note from the new-note folder and counts its missing links there as broken", async () => { + const vault = await mkdtemp(join(tmpdir(), "prompt-daily-blank-folder-")) + onTestFinished(() => rm(vault, { recursive: true, force: true })) + await mkdir(join(vault, ".obsidian")) + await mkdir(join(vault, "Inbox")) + await mkdir(join(vault, "About Me")) + await writeFile(join(vault, ".obsidian", "daily-notes.json"), '{"folder":""}') + // The new-note folder also holds other notes, so a missing note there is + // not taken for a daily note that has not been created yet + await writeFile( + join(vault, ".obsidian", "app.json"), + '{"newFileLocation":"folder","newFileFolderPath":"Inbox"}', + ) + const dailyContent = "# 2026-06-16\n\n[[Inbox/2026-06-17]]\n" + await writeFile(join(vault, "Inbox", "2026-06-16.md"), dailyContent) + const search = createSearchIndex(":memory:") + search.upsertNote( + { + filePath: "Inbox/2026-06-16.md", + rawContent: dailyContent, + fileStat: { mtimeMs: JUNE_16_MIDDAY_MS, size: Buffer.byteLength(dailyContent) }, + }, + logger, + ) + const handler = findCall(registerWithSearch(vault, search), PROMPT_NAMES.DAILY_REVIEW)[2] + + const text = textOf(await handler({ date: "2026-06-16" }, fakeExtra)) + + expect({ + leadLine: reviewLeadLine(text), + outgoingLinks: reviewSection(text, "Outgoing links"), + }).toEqual({ + leadLine: "Daily note: `Inbox/2026-06-16.md`", + outgoingLinks: + "- Inbox/2026-06-17 (**broken** — target does not exist)\n\n1 broken link — the target note does not exist yet.", + }) + }) + it("shows backlinks when other notes reference the daily note", async () => { const vault = await mkdtemp(join(tmpdir(), "prompt-daily-back-")) onTestFinished(async () => { diff --git a/src/vault-mcp/mcp-core/prompts/__tests__/vault-orientation-prompt.test.ts b/src/vault-mcp/mcp-core/prompts/__tests__/vault-orientation-prompt.test.ts index 01b1fdb10..16b5d1897 100644 --- a/src/vault-mcp/mcp-core/prompts/__tests__/vault-orientation-prompt.test.ts +++ b/src/vault-mcp/mcp-core/prompts/__tests__/vault-orientation-prompt.test.ts @@ -116,6 +116,26 @@ describe("vault-orientation live orphan folders", () => { ) }) + it("neither excludes nor counts forward-refs in the new-note folder a blank daily folder resolves to", async () => { + const { vault, readSurveySections } = await setupOrphanPrompt({ + settings: '{"folder":""}', + paths: ["source.md", "Inbox/idea.md", "Templates/template.md"], + noteContents: { "source.md": "[[Inbox/2026-10-07]] [[missing]]" }, + }) + // Daily notes then go to Inbox alongside other new notes, so Inbox is not + // a daily-only folder + await writeFile( + join(vault, ".obsidian/app.json"), + '{"newFileLocation":"folder","newFileFolderPath":"Inbox"}', + ) + + expect(await readSurveySections()).toEqual({ + orphans: "2 orphan notes (no incoming links):\n- source.md — source\n- Inbox/idea.md — idea", + stats: + "3 notes across 0 folders, 0 tags, 0 property keys. 3 untagged. 3 without properties. 2 broken links.", + }) + }) + it("keeps ordinary orphans visible behind more than five newer excluded daily candidates", async () => { const dailyPaths = Array.from({ length: 7 }, (_, index) => `Journal/nested/day-${index}.md`) const { orphanSection } = await setupOrphanPrompt({ diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index ae02ac206..fea6ac4eb 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -127,10 +127,14 @@ describe("readDailyNotesConfig", () => { it("uses the vault root when app.json does not exist, as Obsidian's default", async () => { const vaultDir = await createVault() await writeDailyNotesConfig(vaultDir, { folder: "", format: "YYYY-MM-DD" }) + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + // The unreadable-app.json fallback also gives the root, but with a warn + expect(warnSpy).not.toHaveBeenCalled() }) it("uses the folder app.json sets for new notes, without treating it as daily-only", async () => { @@ -147,10 +151,49 @@ describe("readDailyNotesConfig", () => { const vaultDir = await createVault() await writeDailyNotesConfig(vaultDir, { folder: "" }) await writeAppConfig(vaultDir, { newFileLocation: "current", newFileFolderPath: "Inbox" }) + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + expect(warnSpy).not.toHaveBeenCalled() + }) + + it.each([ + { + label: "a non-string folder path", + appConfig: { newFileLocation: "folder", newFileFolderPath: 42 }, + }, + { label: "no folder path", appConfig: { newFileLocation: "folder" } }, + { label: "the JSON literal null", appConfig: null }, + ])( + "uses the vault root, without a warn, for an app.json with $label", + async ({ appConfig }) => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "" }) + await writeAppConfig(vaultDir, appConfig) + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) + + expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + expect(warnSpy).not.toHaveBeenCalled() + }, + ) + + it("uses the vault root, without a warn, when app.json is a directory", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "" }) + await mkdir(join(vaultDir, ".obsidian", "app.json")) + const warnSpy = vi.spyOn(logger, "warn") + onTestFinished(() => warnSpy.mockRestore()) const config = await readDailyNotesConfig({ vaultPath: vaultDir }, logger) expect(config).toEqual({ folder: "", format: "YYYY-MM-DD", dedicatedFolder: null }) + expect(warnSpy).not.toHaveBeenCalled() }) it("reads a file holding the JSON literal null as a blank folder, without a warn", async () => { From 4aef3f4df5f2b180eafff86af1855871b46be08a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:58:21 -0400 Subject: [PATCH 08/26] refactor(daily-notes): take a resolved config through its own settings variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daily-review prompt passed its resolved config back in through the env-settings parameter. getDailyNote and getDailyNotePath now take a settings union (env or resolved), so each value has one meaning. No behaviour change. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../mcp-core/prompts/daily-review-prompt.ts | 9 ++-- .../mcp-core/tools/daily-note-tools.ts | 9 ++-- .../__tests__/daily-notes.test.ts | 14 ++++-- src/vault-mcp/vault-operations/daily-notes.ts | 45 ++++++++++--------- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts b/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts index 8e926215d..afeeec881 100644 --- a/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/daily-review-prompt.ts @@ -198,8 +198,7 @@ export const registerDailyReviewPrompt = ({ } // Resolved once so the note path and the link classification below - // see the same settings; passing both fields makes getDailyNote skip - // its own read of daily-notes.json. + // see the same settings. const dailyNotesConfig = await readDailyNotesConfig( { vaultPath, @@ -208,7 +207,11 @@ export const registerDailyReviewPrompt = ({ reqLogger, ) const dailyNote = await getDailyNote( - { vaultPath, date: resolvedDate, envSettings: dailyNotesConfig }, + { + vaultPath, + date: resolvedDate, + settings: { source: "resolved", config: dailyNotesConfig }, + }, reqLogger, ) // One note past the cap tells the review whether it lists every note diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index 480860f8e..de940564a 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -58,9 +58,12 @@ Returns: JSON with path (string — resolved vault-relative path), content (stri { vaultPath, date, - envSettings: { - folder: config.dailyNotesFolder, - format: config.dailyNotesFormat, + settings: { + source: "env", + envSettings: { + folder: config.dailyNotesFolder, + format: config.dailyNotesFormat, + }, }, }, reqLogger, diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index fea6ac4eb..b9f967aa1 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -538,16 +538,19 @@ describe("getDailyNotePath", () => { expect(path).toBe("13-05-2026.md") }) - it("resolves a vault-root path from a resolved config passed back as settings", async () => { + it("uses a resolved config as is, reading no config file", async () => { const vaultDir = await createVault() - // A folder setting the shortcut must skip, so a file read would change the path + // A different folder and format on disk, so a file read would change the path await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "YYYY-MM-DD" }) const path = await getDailyNotePath( { vaultPath: vaultDir, date: "2026-05-13", - envSettings: { folder: "", format: "DD-MM-YYYY" }, + settings: { + source: "resolved", + config: { folder: "", format: "DD-MM-YYYY", dedicatedFolder: null }, + }, }, logger, ) @@ -563,7 +566,10 @@ describe("getDailyNotePath", () => { { vaultPath: vaultDir, date: "2026-05-13", - envSettings: { folder: "Override Folder", format: "DD-MM-YYYY" }, + settings: { + source: "env", + envSettings: { folder: "Override Folder", format: "DD-MM-YYYY" }, + }, }, logger, ) diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index ef0a89606..d1dcd069d 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -45,14 +45,20 @@ type DailyNotesFileSettings = { } /** Per-field settings from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env - * vars; each set field takes precedence over .obsidian/daily-notes.json. - * loadConfig never yields a blank folder, so `folder: ""` arrives only from - * a resolved DailyNotesConfig passed back in, and means the vault root. */ + * vars; each set field takes precedence over .obsidian/daily-notes.json. */ export type DailyNotesEnvSettings = { folder?: string | undefined format?: string | undefined } +/** Where a daily note's folder and format come from: + * - `env`: the env settings, with unset fields read from the vault's config. + * - `resolved`: a config readDailyNotesConfig already returned, used as is, + * so a caller that needs the config itself reads the files only once. */ +type DailyNotesSettings = + | { source: "env"; envSettings: DailyNotesEnvSettings } + | { source: "resolved"; config: DailyNotesConfig } + // The server's own choice for a vault with no daily-notes.json. Obsidian then // uses its location for new notes, which is not a sensible folder to assume. export const DEFAULT_DAILY_NOTES_FOLDER = "Daily Notes" @@ -226,16 +232,12 @@ export const readDailyNotesConfig = async ( logger: Logger, ): Promise => { const { vaultPath, envSettings } = params - const envFormat = envSettings?.format - - // "" is a set folder here, the vault root (see DailyNotesEnvSettings), so - // this checks for undefined; `|| null` below then marks the root as shared. const envFolder = envSettings?.folder - const hasEnvFolder = envFolder !== undefined + const envFormat = envSettings?.format // Both fields set — the files can't contribute anything, skip I/O. - if (hasEnvFolder && envFormat) { - return { folder: envFolder, format: envFormat, dedicatedFolder: envFolder || null } + if (envFolder && envFormat) { + return { folder: envFolder, format: envFormat, dedicatedFolder: envFolder } } const { folderSetting, format: fileFormat } = await readDailyNotesFileOrFallback( @@ -244,8 +246,9 @@ export const readDailyNotesConfig = async ( ) const format = envFormat ?? fileFormat - if (hasEnvFolder) { - return { folder: envFolder, format, dedicatedFolder: envFolder || null } + // loadConfig trims DAILY_NOTES_FOLDER, so a set value is never blank. + if (envFolder) { + return { folder: envFolder, format, dedicatedFolder: envFolder } } const folder = await resolveDailyNotesFolder({ folderSetting, vaultPath }, logger) @@ -264,19 +267,21 @@ export const describeNonCalendarDate = (date: string): string => { return `"${date}" is not a calendar date. Pass a real date in YYYY-MM-DD format.` } -/** Resolves a date to a vault-relative daily note path using the env - * settings, the vault's .obsidian/daily-notes.json config, and - * the fallbacks — in that per-field precedence order. */ +/** Resolves a date to a vault-relative daily note path. Without settings, + * the folder and format come from the vault's config and the fallbacks. */ export const getDailyNotePath = async ( params: { vaultPath: string date?: string | undefined - envSettings?: DailyNotesEnvSettings | undefined + settings?: DailyNotesSettings | undefined }, logger: Logger, ): Promise => { - const { vaultPath, date, envSettings } = params - const config = await readDailyNotesConfig({ vaultPath, envSettings }, logger) + const { vaultPath, date, settings } = params + const config = + settings?.source === "resolved" + ? settings.config + : await readDailyNotesConfig({ vaultPath, envSettings: settings?.envSettings }, logger) const unsupportedTokens = findUnsupportedTokens(config.format) @@ -317,7 +322,7 @@ export const getDailyNote = async ( params: { vaultPath: string date?: string | undefined - envSettings?: DailyNotesEnvSettings | undefined + settings?: DailyNotesSettings | undefined }, logger: Logger, ): Promise => { @@ -325,7 +330,7 @@ export const getDailyNote = async ( { vaultPath: params.vaultPath, date: params.date, - envSettings: params.envSettings, + settings: params.settings, }, logger, ) From af2f3e379fe4f88a9c2da17ea06771c3d4e5bfe7 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:12:04 -0400 Subject: [PATCH 09/26] fix(daily-notes): normalize the folder setting like Obsidian's normalizePath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obsidian's daily notes plugin passes its folder setting through normalizePath, which also turns no-break spaces into plain spaces and NFC-normalizes the name. The server only collapsed slashes, so a folder setting holding either form named a folder Obsidian never uses. The folder-list helper in vault-folder-config.ts was a type predicate that returned false for whitespace-only strings, so it typed those strings as null in its false branch. It now returns the folder or null. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 2 +- .../__tests__/daily-notes.test.ts | 3 +++ src/vault-mcp/vault-operations/daily-notes.ts | 19 ++++++++++++++----- .../vault-operations/vault-folder-config.ts | 19 ++++++++++--------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8b54b7e8a..e81e338e1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -246,7 +246,7 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag **Daily notes:** `vault_get_daily_note` resolves the vault's folder and date format, each independently: `DAILY_NOTES_FOLDER`/`DAILY_NOTES_FORMAT` env setting → `.obsidian/daily-notes.json` → fallback (`Daily Notes/YYYY-MM-DD.md`). - **Read on every call:** a folder or format change in Obsidian applies to the next call, and a config file that arrives after boot needs no restart -- **Folder normalized like Obsidian:** runs of `/` or `\` become one `/` and leading and trailing slashes are dropped, so Obsidian's `"/"` is the vault root +- **Folder normalized like Obsidian:** runs of `/` or `\` become one `/` and leading and trailing slashes are dropped, so Obsidian's `"/"` is the vault root; no-break spaces become plain spaces and the name is NFC-normalized - **Blank folder in the file:** Obsidian's new-note location (`newFileLocation: "folder"` plus `newFileFolderPath` in `.obsidian/app.json`), otherwise the vault root - **Missing file:** takes the fallbacks - **Unparseable file:** takes the fallbacks and logs a `warn` diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index b9f967aa1..90ca381d7 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -110,6 +110,9 @@ describe("readDailyNotesConfig", () => { { label: "leading and trailing slashes", setting: "/Journal/", expected: "Journal" }, { label: "repeated slashes", setting: "Journal//2026", expected: "Journal/2026" }, { label: "backslashes", setting: "Journal\\2026", expected: "Journal/2026" }, + { label: "no-break spaces", setting: "Daily\u00A0Notes\u202FLog", expected: "Daily Notes Log" }, + // "e" plus a combining acute accent, composed into "é" + { label: "decomposed accents", setting: "Journe\u0301es", expected: "Journ\u00E9es" }, ])("normalizes $label in the folder the way Obsidian does", async ({ setting, expected }) => { const vaultDir = await createVault() await writeDailyNotesConfig(vaultDir, { folder: setting, format: "YYYY-MM-DD" }) diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index d1dcd069d..2c91d7d09 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -76,12 +76,21 @@ const FALLBACK_FILE_SETTINGS: DailyNotesFileSettings = { /** Matches each run of forward or back slashes in a folder setting. */ const SLASH_RUN_RE = /[\\/]+/g -/** Normalizes a folder setting the way Obsidian does before using it: - * slash runs (either direction) become one "/", and leading and trailing - * slashes are dropped. Obsidian saves the vault root as "/", which - * becomes "". */ +/** Matches the no-break space (U+00A0) and the narrow no-break space + * (U+202F), which Obsidian turns into plain spaces in a path. */ +const NO_BREAK_SPACE_RE = /[\u00A0\u202F]/g + +/** Normalizes a folder setting the way Obsidian's normalizePath does before + * using it: + * - Slash runs (either direction) become one "/", and leading and trailing + * slashes are dropped. Obsidian saves the vault root as "/", which becomes "". + * - No-break spaces become plain spaces, and the result is NFC-normalized. */ const normalizeFolderSetting = (folderSetting: string): string => { - return folderSetting.replace(SLASH_RUN_RE, "/").replace(/^\/|\/$/g, "") + return folderSetting + .replace(SLASH_RUN_RE, "/") + .replace(/^\/|\/$/g, "") + .replace(NO_BREAK_SPACE_RE, " ") + .normalize("NFC") } /** Reads and parses .obsidian/daily-notes.json. A missing file gives the diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index 16aad4138..0284dd237 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -2,10 +2,10 @@ import type { VaultConfig } from "../config.js" import type { Logger } from "../../logger.js" import { readDailyNotesConfig, readProtectedDailyNotesFolder } from "./daily-notes.js" -/** False for null (a vault-root or blank setting) and for a whitespace-only - * setting: neither names a daily folder to add. */ -const isDailyFolderSet = (dailyFolder: string | null): dailyFolder is string => { - return Boolean(dailyFolder?.trim()) +/** The daily folder to add to a default list, or null when there is none to + * add: a vault-root or blank setting (null) and a whitespace-only one. */ +const pickDailyFolderToAdd = (dailyFolder: string | null): string | null => { + return dailyFolder?.trim() ? dailyFolder : null } /** The folders that delete and move refuse to touch. @@ -30,9 +30,10 @@ export const resolveEffectiveProtectedPaths = async ( // loadConfig trims DAILY_NOTES_FOLDER, so a set value is never blank. if (config.dailyNotesFolder) return [config.memoryDir, config.dailyNotesFolder] - const dailyFolder = await readProtectedDailyNotesFolder(vaultPath, logger) + const protectedDailyFolder = await readProtectedDailyNotesFolder(vaultPath, logger) + const dailyFolder = pickDailyFolderToAdd(protectedDailyFolder) - return isDailyFolderSet(dailyFolder) ? [config.memoryDir, dailyFolder] : [config.memoryDir] + return dailyFolder ? [config.memoryDir, dailyFolder] : [config.memoryDir] } /** The folders orphan results leave out: ORPHAN_EXCLUDE_FOLDERS when set, @@ -49,9 +50,9 @@ export const resolveEffectiveOrphanExcludeFolders = ({ }): readonly string[] => { if (orphanExcludeFoldersOverride) return orphanExcludeFoldersOverride - return isDailyFolderSet(dailyNotesFolder) - ? [dailyNotesFolder, "Templates", memoryDir] - : ["Templates", memoryDir] + const dailyFolder = pickDailyFolderToAdd(dailyNotesFolder) + + return dailyFolder ? [dailyFolder, "Templates", memoryDir] : ["Templates", memoryDir] } /** resolveEffectiveOrphanExcludeFolders for callers holding only the config: From 6b4a1c6121f070964ad66badf803202b12aef6fb Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:12:22 -0400 Subject: [PATCH 10/26] fix(daily-notes): trim the formatted date in the daily note filename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obsidian trims the date it formats from the daily note format before building the path, so a format with a space at either end still names "2026-05-13.md". The server kept the spaces and reported such a note as missing. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../vault-operations/__tests__/daily-notes.test.ts | 9 +++++++++ src/vault-mcp/vault-operations/daily-notes.ts | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index 90ca381d7..0c05202c0 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -541,6 +541,15 @@ describe("getDailyNotePath", () => { expect(path).toBe("13-05-2026.md") }) + it("trims spaces the format leaves at either end of the filename, as Obsidian does", async () => { + const vaultDir = await createVault() + await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: " YYYY-MM-DD " }) + + const path = await getDailyNotePath({ vaultPath: vaultDir, date: "2026-05-13" }, logger) + + expect(path).toBe("Journal/2026-05-13.md") + }) + it("uses a resolved config as is, reading no config file", async () => { const vaultDir = await createVault() // A different folder and format on disk, so a file read would change the path diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index 2c91d7d09..5d8c7a184 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -315,7 +315,10 @@ export const getDailyNotePath = async ( throw new Error(describeNonCalendarDate(date)) } - const filename = dateTime.toFormat(luxonFormat) + // Obsidian trims the formatted date before building the path, so a format + // with a space at either end still names "2026-05-13.md". + const filename = dateTime.toFormat(luxonFormat).trim() + return config.folder ? `${config.folder}/${filename}.md` : `${filename}.md` } From 6c20959b6b66a6974811930aba7619c23dc72873 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:12:31 -0400 Subject: [PATCH 11/26] fix(docs): describe root and empty daily folders in env comments and DEPLOY.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The env-file comments, the CLI's generated .env blocks and DEPLOY.md said the daily notes folder falls back to "Daily Notes" and is always protected and excluded from orphans. An empty folder setting now follows Obsidian's new-note location, and a vault-root or empty setting adds no protected or excluded folder. - The SYNC_CONFIGS comments and the remote guide called the defaults the two categories the server reads; the server now also reads app.json for an empty folder setting. - The remote guide gives the full SYNC_CONFIGS value to set when adding app, since a set value replaces the default list. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .env.example | 24 ++++++++++++---------- DEPLOY.md | 6 +++--- cli/src/env.ts | 41 ++++++++++++++++++++++---------------- deploy/local/.env.example | 17 +++++++++------- deploy/remote/.env.example | 24 ++++++++++++---------- deploy/remote/README.md | 20 +++++++++---------- 6 files changed, 75 insertions(+), 57 deletions(-) diff --git a/.env.example b/.env.example index c63738bef..286ec23eb 100644 --- a/.env.example +++ b/.env.example @@ -70,8 +70,8 @@ GHCR_USER= # default list) and turn on "Sync all other types" on the uploading device. # SYNC_FILE_TYPES= # Obsidian settings categories to sync into the server's .obsidian/ folder -# (default: the two the server reads — daily notes settings and community -# plugin settings such as the Tasks plugin's format; "none" disables). +# (default: daily notes settings and community plugin settings such as the +# Tasks plugin's format; "none" disables). # A category only syncs after your desktop pushes it: Settings → Sync → # "Vault configuration sync". Some community plugins keep API keys in their # settings — server tools never read .obsidian/, but synced settings do @@ -220,22 +220,26 @@ GHCR_USER= # DISABLED_TOOLS=vault_delete_note,vault_move_note # Memory folder name in your vault (default: "About Me") # MEMORY_DIR=About Me -# Daily notes folder and filename format (default: read from the -# vault's .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS -# above; falls back to "Daily Notes" and YYYY-MM-DD). Folder is any -# vault-relative path (Journal, Planner/Daily); format takes the same tokens -# as Obsidian's date format setting. +# Daily notes folder and filename format. Default: read from the vault's +# .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS above, or +# "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An +# empty folder in that file means Obsidian's location for new notes, which +# syncs only when SYNC_CONFIGS includes app; without it, the server uses the +# vault root. Folder is any vault-relative path (Journal, Planner/Daily); +# format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json -# (default "Daily Notes"). When set, replaces the whole default — include the -# daily notes folder in your list if needed. +# (default "Daily Notes"; none when that setting is the vault root or empty). +# When set, replaces the whole default — include the daily notes folder in +# your list if needed. # PROTECTED_PATHS=About Me,Daily Notes # Comma-separated folders excluded from orphan detection. # Default: the daily notes folder, Templates, and MEMORY_DIR. # DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is reread -# on each query, falling back to "Daily Notes". Root-level daily notes remain eligible. +# on each query, falling back to "Daily Notes". A vault-root or empty folder +# setting adds no daily folder, so those daily notes remain eligible. # When set, replaces the whole default — include every folder to keep excluded. # Set ORPHAN_EXCLUDE_FOLDERS=, to exclude nothing; an empty value uses the defaults. # Apply env changes by redeploying; synced Obsidian settings apply on the next query. diff --git a/DEPLOY.md b/DEPLOY.md index 571baed44..5f3252b2e 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -364,13 +364,13 @@ To find your stage: `cat .sst/stage` (after one-time setup). | `READONLY_MODE` | Optional. Set `true` to hide every tool that changes the vault and skip memory folder auto-creation — read and search only. Default: `false`. | | `DISABLED_TOOLS` | Optional. Hide individual tools by name, comma-separated. Names match the Tool column in the [README tools table](./README.md#tools). Subtractive only; an unknown tool name stops the server at startup. Default: none hidden. | | `MEMORY_DIR` | Optional. Memory folder name in the vault (default: `About Me`). See the [Configuration](./README.md#configuration) section. | -| `PROTECTED_PATHS` | Optional. Comma-separated folders protected from deletion and moves (default: `MEMORY_DIR` plus the daily notes folder, read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json`, default `Daily Notes`). Overrides the default entirely when set. | -| `ORPHAN_EXCLUDE_FOLDERS` | Optional. Comma-separated folders excluded from orphan detection (default: the daily notes folder, `Templates`, and `MEMORY_DIR`). The `DAILY_NOTES_FOLDER` variable wins; otherwise synced `.obsidian/daily-notes.json` is reread per query (fallback `Daily Notes`). Overrides the default entirely when set. Use `,` to exclude nothing; an empty value uses the defaults. Apply variable changes with Force Deploy; synced Obsidian changes need no redeploy. See [Daily notes](./README.md#daily-notes) for root-level notes. | +| `PROTECTED_PATHS` | Optional. Comma-separated folders protected from deletion and moves (default: `MEMORY_DIR` plus the daily notes folder, read from `DAILY_NOTES_FOLDER` or `.obsidian/daily-notes.json`, default `Daily Notes`; none when that setting is the vault root or empty). Overrides the default entirely when set. | +| `ORPHAN_EXCLUDE_FOLDERS` | Optional. Comma-separated folders excluded from orphan detection (default: the daily notes folder, `Templates`, and `MEMORY_DIR`). The `DAILY_NOTES_FOLDER` variable wins; otherwise synced `.obsidian/daily-notes.json` is reread per query (fallback `Daily Notes`). Overrides the default entirely when set. Use `,` to exclude nothing; an empty value uses the defaults. Apply variable changes with Force Deploy; synced Obsidian changes need no redeploy. See [Daily notes](./README.md#daily-notes) for root and empty settings. | | `SERVICE_DOCUMENTATION_URL` | Optional. URL in OAuth discovery metadata (default: `https://github.com/aliasunder/vault-cortex`). Set to your fork's URL. | | `SYNC_CONFIGS` | Optional. Obsidian settings categories synced to the server, comma-separated (default: `core-plugin-data,community-plugin-data` — daily-notes settings and the Tasks plugin's format). Set `none` to disable. See [Daily notes](./README.md#daily-notes). | | `SYNC_EXCLUDED_FOLDERS` | Optional. Folders to leave out of Obsidian Sync, comma-separated — the same list as Obsidian's Sync → Excluded folders. Unset syncs everything. | | `SYNC_FILE_TYPES` | Optional. Attachment types Obsidian Sync delivers, comma-separated. **Valid values:** `image`, `audio`, `video`, `pdf`, `unsupported`. **Unset (default):** `image,audio,video,pdf`. The text files Vault Cortex reads (CSV, JSON, TXT, XML, LOG, YAML) fall under `unsupported`, Obsidian Sync's name for its **Sync all other types** category; SVG syncs as `image`, and notes, canvases, and bases always sync. To read those text files, set it to `image,audio,video,pdf,unsupported` (the value replaces the default list, so keep the four defaults) and turn on **Sync all other types** in Obsidian's Sync settings on the device that has them. An invalid value is skipped with a warning in the logs, and the default types sync instead. | -| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder (default: read from the vault's `.obsidian/daily-notes.json`, falling back to `Daily Notes`). See [Daily notes](./README.md#daily-notes). | +| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder (default: read from the vault's `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable). See [Daily notes](./README.md#daily-notes). | | `DAILY_NOTES_FORMAT` | Optional. Sets the daily note filename format, in the same tokens as Obsidian's daily note date format setting (default: read from the vault's config, falling back to `YYYY-MM-DD`). | | `TZ` | Optional. Container timezone (default: `UTC`). Affects `vault_update_memory` date stamps and `vault_get_daily_note` date resolution. Set to your IANA timezone (e.g. `America/New_York`). | | `LOG_LEVEL` | Optional. Logging verbosity: `debug`, `info`, `warn`, `error`. Default: `info`. | diff --git a/cli/src/env.ts b/cli/src/env.ts index c07535ad3..cb6240d34 100644 --- a/cli/src/env.ts +++ b/cli/src/env.ts @@ -94,23 +94,26 @@ READONLY_MODE=false # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me -# Daily notes folder and filename format (default: read from the -# vault's .obsidian/daily-notes.json, falling back to "Daily Notes" and -# YYYY-MM-DD). Folder is any vault-relative path (Journal, Planner/Daily); -# format takes the same tokens as Obsidian's date format setting. +# Daily notes folder and filename format. Default: read from the vault's +# .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file +# is missing or unreadable. An empty folder in that file means Obsidian's +# location for new notes. Folder is any vault-relative path (Journal, +# Planner/Daily); format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json -# (default "Daily Notes"). When set, replaces the whole default — include the -# daily notes folder in your list if needed. +# (default "Daily Notes"; none when that setting is the vault root or empty). +# When set, replaces the whole default — include the daily notes folder in +# your list if needed. # PROTECTED_PATHS=About Me,Daily Notes # Comma-separated folders excluded from orphan detection. # Default: the daily notes folder, Templates, and MEMORY_DIR. # DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is reread -# on each query, falling back to "Daily Notes". Root-level daily notes remain eligible. +# on each query, falling back to "Daily Notes". A vault-root or empty folder +# setting adds no daily folder, so those daily notes remain eligible. # When set, replaces the whole default — include every folder to keep excluded. # Set ORPHAN_EXCLUDE_FOLDERS=, to exclude nothing; an empty value uses the defaults. # To apply environment changes, run the CLI's restart command @@ -242,24 +245,28 @@ READONLY_MODE=false # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me -# Daily notes folder and filename format (default: read from the -# vault's .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS -# below; falls back to "Daily Notes" and YYYY-MM-DD). Folder is any -# vault-relative path (Journal, Planner/Daily); format takes the same tokens -# as Obsidian's date format setting. +# Daily notes folder and filename format. Default: read from the vault's +# .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS below, or +# "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An +# empty folder in that file means Obsidian's location for new notes, which +# syncs only when SYNC_CONFIGS includes app; without it, the server uses the +# vault root. Folder is any vault-relative path (Journal, Planner/Daily); +# format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json -# (default "Daily Notes"). When set, replaces the whole default — include the -# daily notes folder in your list if needed. +# (default "Daily Notes"; none when that setting is the vault root or empty). +# When set, replaces the whole default — include the daily notes folder in +# your list if needed. # PROTECTED_PATHS=About Me,Daily Notes # Comma-separated folders excluded from orphan detection. # Default: the daily notes folder, Templates, and MEMORY_DIR. # DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is reread -# on each query, falling back to "Daily Notes". Root-level daily notes remain eligible. +# on each query, falling back to "Daily Notes". A vault-root or empty folder +# setting adds no daily folder, so those daily notes remain eligible. # When set, replaces the whole default — include every folder to keep excluded. # Set ORPHAN_EXCLUDE_FOLDERS=, to exclude nothing; an empty value uses the defaults. # To apply environment changes, run the CLI's restart command @@ -347,8 +354,8 @@ SYNC_EXCLUDED_FOLDERS= SYNC_FILE_TYPES= # Obsidian settings categories to sync into the server's .obsidian/ folder -# (default: the two the server reads — daily notes settings and community -# plugin settings such as the Tasks plugin's format; "none" disables). +# (default: daily notes settings and community plugin settings such as the +# Tasks plugin's format; "none" disables). # A category only syncs after your desktop pushes it: Obsidian Settings → # Sync → "Vault configuration sync" (per device). Some community plugins # keep API keys in their settings — server tools never read .obsidian/, diff --git a/deploy/local/.env.example b/deploy/local/.env.example index 7e4fde2dd..87ce2d0b8 100644 --- a/deploy/local/.env.example +++ b/deploy/local/.env.example @@ -84,23 +84,26 @@ READONLY_MODE=false # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me -# Daily notes folder and filename format (default: read from the -# vault's .obsidian/daily-notes.json, falling back to "Daily Notes" and -# YYYY-MM-DD). Folder is any vault-relative path (Journal, Planner/Daily); -# format takes the same tokens as Obsidian's date format setting. +# Daily notes folder and filename format. Default: read from the vault's +# .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file +# is missing or unreadable. An empty folder in that file means Obsidian's +# location for new notes. Folder is any vault-relative path (Journal, +# Planner/Daily); format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json -# (default "Daily Notes"). When set, replaces the whole default — include the -# daily notes folder in your list if needed. +# (default "Daily Notes"; none when that setting is the vault root or empty). +# When set, replaces the whole default — include the daily notes folder in +# your list if needed. # PROTECTED_PATHS=About Me,Daily Notes # Comma-separated folders excluded from orphan detection. # Default: the daily notes folder, Templates, and MEMORY_DIR. # DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is reread -# on each query, falling back to "Daily Notes". Root-level daily notes remain eligible. +# on each query, falling back to "Daily Notes". A vault-root or empty folder +# setting adds no daily folder, so those daily notes remain eligible. # When set, replaces the whole default — include every folder to keep excluded. # Set ORPHAN_EXCLUDE_FOLDERS=, to exclude nothing; an empty value uses the defaults. # To apply environment changes, run the CLI's restart command diff --git a/deploy/remote/.env.example b/deploy/remote/.env.example index 04fe00eda..baffaf5f1 100644 --- a/deploy/remote/.env.example +++ b/deploy/remote/.env.example @@ -89,24 +89,28 @@ READONLY_MODE=false # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me -# Daily notes folder and filename format (default: read from the -# vault's .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS -# below; falls back to "Daily Notes" and YYYY-MM-DD). Folder is any -# vault-relative path (Journal, Planner/Daily); format takes the same tokens -# as Obsidian's date format setting. +# Daily notes folder and filename format. Default: read from the vault's +# .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS below, or +# "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An +# empty folder in that file means Obsidian's location for new notes, which +# syncs only when SYNC_CONFIGS includes app; without it, the server uses the +# vault root. Folder is any vault-relative path (Journal, Planner/Daily); +# format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus # the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json -# (default "Daily Notes"). When set, replaces the whole default — include the -# daily notes folder in your list if needed. +# (default "Daily Notes"; none when that setting is the vault root or empty). +# When set, replaces the whole default — include the daily notes folder in +# your list if needed. # PROTECTED_PATHS=About Me,Daily Notes # Comma-separated folders excluded from orphan detection. # Default: the daily notes folder, Templates, and MEMORY_DIR. # DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is reread -# on each query, falling back to "Daily Notes". Root-level daily notes remain eligible. +# on each query, falling back to "Daily Notes". A vault-root or empty folder +# setting adds no daily folder, so those daily notes remain eligible. # When set, replaces the whole default — include every folder to keep excluded. # Set ORPHAN_EXCLUDE_FOLDERS=, to exclude nothing; an empty value uses the defaults. # To apply environment changes, run the CLI's restart command @@ -194,8 +198,8 @@ SYNC_EXCLUDED_FOLDERS= SYNC_FILE_TYPES= # Obsidian settings categories to sync into the server's .obsidian/ folder -# (default: the two the server reads — daily notes settings and community -# plugin settings such as the Tasks plugin's format; "none" disables). +# (default: daily notes settings and community plugin settings such as the +# Tasks plugin's format; "none" disables). # A category only syncs after your desktop pushes it: Obsidian Settings → # Sync → "Vault configuration sync" (per device). Some community plugins # keep API keys in their settings — server tools never read .obsidian/, diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 26c6bf282..78604eb94 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -539,13 +539,13 @@ in `.obsidian/daily-notes.json`, and in remote mode the server only receives that file when settings syncing is turned on in two places: - **Server side** — `SYNC_CONFIGS` defaults to - `core-plugin-data,community-plugin-data`, the two categories the server - reads: daily notes settings (Obsidian's **Core plugin settings** toggle) - and community plugin settings, such as the Tasks plugin's format - (**Installed community plugins**). It takes a comma-separated list of - Obsidian's settings categories (`app`, `appearance`, `appearance-data`, - `hotkey`, `core-plugin`, `core-plugin-data`, `community-plugin`, - `community-plugin-data`); set `SYNC_CONFIGS=none` in `.env` to disable. + `core-plugin-data,community-plugin-data`: daily notes settings (Obsidian's + **Core plugin settings** toggle) and community plugin settings, such as + the Tasks plugin's format (**Installed community plugins**). It takes a + comma-separated list of Obsidian's settings categories (`app`, + `appearance`, `appearance-data`, `hotkey`, `core-plugin`, + `core-plugin-data`, `community-plugin`, `community-plugin-data`); set + `SYNC_CONFIGS=none` in `.env` to disable. - **Desktop side** — Obsidian Settings → Sync → **Vault configuration sync**, enabled per device. Obsidian only pushes categories you enable there. @@ -565,9 +565,9 @@ the server then looks in the vault root. **Same folder as current file** also means the vault root, as it does in Obsidian for daily notes. If your new notes go to a set folder, either: -- add `app` to `SYNC_CONFIGS` and turn on **Main settings** under Vault - configuration sync — a `.env` edit, so re-create the container as described - below; or +- set `SYNC_CONFIGS=core-plugin-data,community-plugin-data,app`, the default + list plus `app`, and turn on **Main settings** under Vault configuration + sync — a `.env` edit, so re-create the container as described below; or - set `DAILY_NOTES_FOLDER`, as described below. Daily notes in the vault root, or in the new-note folder because the folder From 37487cd06eb9618ecf0fda9fd7f410b635bec570 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:26:40 -0400 Subject: [PATCH 12/26] fix(daily-notes): name the daily notes folder in the new tool-text clauses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool-definition review: "that setting" had no antecedent in the clauses this branch added, and the move tool's protected-path clause sat in nested parentheses. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/__snapshots__/tool-surface/default.json | 8 ++++---- .../__snapshots__/tool-surface/disabled-tools.json | 8 ++++---- .../__snapshots__/tool-surface/embedding-off.json | 8 ++++---- .../tool-surface/file-tools-off+embedding-off.json | 8 ++++---- .../__snapshots__/tool-surface/file-tools-off.json | 8 ++++---- .../tool-surface/memory-off+embedding-off.json | 8 ++++---- .../memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/memory-off+file-tools-off.json | 8 ++++---- .../__snapshots__/tool-surface/memory-off.json | 8 ++++---- .../__snapshots__/tool-surface/obsidian-sync.json | 8 ++++---- .../tool-surface/readonly+embedding-off.json | 4 ++-- .../readonly+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off.json | 4 ++-- .../readonly+memory-off+embedding-off.json | 4 ++-- ...adonly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/readonly.json | 4 ++-- .../mcp-core/__tests__/tool-definitions.test.ts | 10 +++++----- src/vault-mcp/mcp-core/tools/search-tools.ts | 6 +++--- src/vault-mcp/mcp-core/tools/vault-crud-tools.ts | 4 ++-- 21 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 459d77f7b..2f203aefa 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 @@ -194,7 +194,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -394,7 +394,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 83b19d9a6..9f5eccd61 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 1364e5fee..4ec0d931d 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 58e569812..d0c2fba41 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 @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -397,7 +397,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 cd152d5b4..61f6a3ef9 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 7fda0f9b4..b9d4d8b5f 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 77578005a..086271b15 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 @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -317,7 +317,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 52f24d150..d942b4bd3 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 65b3c4462..805e6b491 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 @@ -150,7 +150,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -315,7 +315,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 ad2115dc1..281e4ddc4 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)) cannot be moved.\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 175063578..29afd456a 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 997ea3a9d..3f4b282fb 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -133,7 +133,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 e361623e9..8d5baf939 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 179828a7c..ca46f5aa2 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 b068d5361..02e571b5e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -99,7 +99,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 263389a7e..f887246ec 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 79967d02e..e4f37d059 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -97,7 +97,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 d4a9b6886..50622a090 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -131,7 +131,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 602539475..160a4ebfc 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -517,7 +517,7 @@ describe("config interpolation in descriptions", () => { linePrefix: "- Links to the note", }) expect(linksEntry).toBe( - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank).", + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: Profile/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).", ) }) @@ -1158,7 +1158,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { }) } const LINKS_AND_PROTECTED_PATHS_ENTRY = - "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)." + "- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank)." it("describes the Deleted files outcomes when the server does not sync", () => { expect(deleteNoteOpener({})).toBe( @@ -1838,7 +1838,7 @@ describe("vault_find_orphans live folder defaults", () => { ?.split("\n") .find((line) => line.startsWith("- With exclude_folders")) expect(defaultsLine).toBe( - '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.', + '- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.', ) expect(toolConfig.description).not.toContain("Journal") expect(toolConfig.inputSchema?.exclude_folders?.description).toBe( @@ -2688,7 +2688,7 @@ describe("DISABLED_TOOLS", () => { }) } const PROTECTED_PATHS_SENTENCE = - " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; none when that setting is the vault root or blank)." + " Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank)." expect(linksEntry("")).toBe( `- Links to the note from other notes become broken; list them first with vault_get_backlinks.${PROTECTED_PATHS_SENTENCE}`, @@ -2712,7 +2712,7 @@ describe("DISABLED_TOOLS", () => { '. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank)${ENTRY_END}`, + `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank)${ENTRY_END}`, ) expect(excludeFoldersEntry("vault_get_daily_note")).toBe(`${ENTRY_START}${ENTRY_END}`) }) diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 69ba8fc26..6a256e36c 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -589,7 +589,7 @@ For incoming links (what links TO a note), use vault_get_backlinks. Parameters: - path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds). -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that setting is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. Errors: - "path must end in …" — add the .md or .canvas extension @@ -641,14 +641,14 @@ Errors: : `the daily notes folder, "Templates", ${JSON.stringify(config.memoryDir)}` const orphanDefaultDescription = config.orphanExcludeFoldersOverride ? "With exclude_folders omitted, the server's configured list (the schema default) is used." - : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). It is skipped when that setting is the vault root or blank.' + : 'With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else "Daily Notes" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.' // An override list may not include the daily notes folder, so the hint only // accompanies the built-in defaults. const dailyNotesFolderHint = config.orphanExcludeFoldersOverride ? "" : whenToolEnabledText( "vault_get_daily_note", - " (vault_get_daily_note's path starts with the daily notes folder, unless that setting is blank)", + " (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank)", ) registerTool( diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 6e4df10cf..673745ec0 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -43,7 +43,7 @@ const describeProtectedPaths = (config: VaultConfig): string => { // loadConfig strips trailing slashes from override entries. return config.protectedPathsOverride.map((protectedPath) => protectedPath + "/").join(", ") } - return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/; none when that setting is the vault root or blank)` + return `${config.memoryDir}/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to ${DEFAULT_DAILY_NOTES_FOLDER}/; not protected when it is the vault root or blank)` } /** Sentences that point to other tools, each already "" when its tool is @@ -1211,7 +1211,7 @@ Example: vault_move_note({ old_path: "Inbox/Spec.md", new_path: "Projects/Spec.m Example: vault_move_note({ old_path: "Inbox/Spec.md", new_path: "Projects/Spec.md", prune_empty_folders: true }) — also remove "Inbox" if the move empties it. When to use: Renaming a note or relocating it to a different folder while keeping the link graph intact. -Prefer this over vault_write_note + vault_delete_note, which would orphan every backlink.${bodyOrPropertiesEditSentence} Protected paths (${describeProtectedPaths(config)}) cannot be moved. +Prefer this over vault_write_note + vault_delete_note, which would orphan every backlink.${bodyOrPropertiesEditSentence} Protected paths cannot be moved: ${describeProtectedPaths(config)}. Parameters: - prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian. From 34eec2a0210094f4d6b1eda740ac978a639faa2a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:40:09 -0400 Subject: [PATCH 13/26] fix(daily-notes): key the forward-ref exception on the folder setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../mcp-core/__tests__/__snapshots__/tool-surface/default.json | 2 +- .../__tests__/__snapshots__/tool-surface/disabled-tools.json | 2 +- .../__tests__/__snapshots__/tool-surface/embedding-off.json | 2 +- .../tool-surface/file-tools-off+embedding-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/file-tools-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+embedding-off.json | 2 +- .../tool-surface/memory-off+file-tools-off+embedding-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+file-tools-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/memory-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/obsidian-sync.json | 2 +- .../__snapshots__/tool-surface/readonly+embedding-off.json | 2 +- .../tool-surface/readonly+file-tools-off+embedding-off.json | 2 +- .../__snapshots__/tool-surface/readonly+file-tools-off.json | 2 +- .../tool-surface/readonly+memory-off+embedding-off.json | 2 +- .../readonly+memory-off+file-tools-off+embedding-off.json | 2 +- .../tool-surface/readonly+memory-off+file-tools-off.json | 2 +- .../__snapshots__/tool-surface/readonly+memory-off.json | 2 +- .../mcp-core/__tests__/__snapshots__/tool-surface/readonly.json | 2 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 2f203aefa..1edfd1195 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 @@ -394,7 +394,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 9f5eccd61..41778775a 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 @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 4ec0d931d..fb489aa01 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 @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 d0c2fba41..7ae2bf3c1 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 @@ -397,7 +397,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 61f6a3ef9..66ad7b5c5 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 @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 b9d4d8b5f..a24a3db82 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 @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 086271b15..e638c570b 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 @@ -317,7 +317,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 d942b4bd3..25708e3c4 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 @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 805e6b491..6bc8a2ed6 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 @@ -315,7 +315,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 281e4ddc4..c7adf42fe 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 @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 29afd456a..e629ac7c0 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 @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 3f4b282fb..8d01a5d32 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 @@ -133,7 +133,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 8d5baf939..137e615cc 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 @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 ca46f5aa2..5823a8af8 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 @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 02e571b5e..1e6f482d6 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 @@ -99,7 +99,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 f887246ec..88c4af976 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 @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 e4f37d059..3cc2bf0bb 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 @@ -97,7 +97,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 50622a090..ce321aa0a 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 @@ -131,7 +131,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 6a256e36c..2052efc5d 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -589,7 +589,7 @@ For incoming links (what links TO a note), use vault_get_backlinks. Parameters: - path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds). -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless that folder is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. Errors: - "path must end in …" — add the .md or .canvas extension From a2e05021b536e60ac0eb90762b9eef9204ea72ef Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:50:54 -0400 Subject: [PATCH 14/26] docs(daily-notes): describe the empty and vault-root folder settings in compose comments and server.json Co-Authored-By: Claude Opus 5.5 --- deploy/local/docker-compose.yml | 4 +++- deploy/remote/docker-compose.yml | 4 +++- docker-compose.local.yml | 4 +++- docker-compose.yml | 7 +++++-- server.json | 6 +++--- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/deploy/local/docker-compose.yml b/deploy/local/docker-compose.yml index 497754838..bec60c75b 100644 --- a/deploy/local/docker-compose.yml +++ b/deploy/local/docker-compose.yml @@ -54,8 +54,10 @@ services: # (/ below are the resolved values, defaults "About Me" / "Daily Notes"): # PROTECTED_PATHS default: ", " (blocked from vault_delete_note) # ORPHAN_EXCLUDE_FOLDERS default: ", Templates, " (excluded from vault_find_orphans) + # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex - # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json, else "Daily Notes" + # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder + # there follows Obsidian's new-note location), else "Daily Notes" # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/deploy/remote/docker-compose.yml b/deploy/remote/docker-compose.yml index dc34a1ac5..7591b8ce5 100644 --- a/deploy/remote/docker-compose.yml +++ b/deploy/remote/docker-compose.yml @@ -70,8 +70,10 @@ services: # (/ below are the resolved values, defaults "About Me" / "Daily Notes"): # PROTECTED_PATHS default: ", " (blocked from vault_delete_note) # ORPHAN_EXCLUDE_FOLDERS default: ", Templates, " (excluded from vault_find_orphans) + # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex - # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json, else "Daily Notes" + # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder + # there follows Obsidian's new-note location), else "Daily Notes" # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index f079f8252..14abc1727 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -52,8 +52,10 @@ services: # (/ below are the resolved values, defaults "About Me" / "Daily Notes"): # PROTECTED_PATHS default: ", " # ORPHAN_EXCLUDE_FOLDERS default: ", Templates, " + # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex - # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json, else "Daily Notes" + # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder + # there follows Obsidian's new-note location), else "Daily Notes" # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/docker-compose.yml b/docker-compose.yml index 6fc8b95ac..3251bd444 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,11 +101,14 @@ services: # stand for the resolved values (defaults "About Me" and "Daily Notes"). # A set folder list replaces its whole default. # The daily notes folder is resolved from DAILY_NOTES_FOLDER or - # .obsidian/daily-notes.json, falling back to "Daily Notes". + # .obsidian/daily-notes.json (an empty folder there follows Obsidian's + # new-note location), falling back to "Daily Notes" when that file is missing. # PROTECTED_PATHS default: ", " # ORPHAN_EXCLUDE_FOLDERS default: ", Templates, " + # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex - # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json, else "Daily Notes" + # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder + # there follows Obsidian's new-note location), else "Daily Notes" # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/server.json b/server.json index f1dd150c0..c6d3fb78f 100644 --- a/server.json +++ b/server.json @@ -118,7 +118,7 @@ }, { "name": "DAILY_NOTES_FOLDER", - "description": "Vault folder for daily notes. Overrides the folder configured in Obsidian's daily-notes plugin. When unset, read from the vault's .obsidian/daily-notes.json, falling back to \"Daily Notes\".", + "description": "Vault folder for daily notes. Overrides the folder configured in Obsidian's daily-notes plugin. When unset, read from the vault's .obsidian/daily-notes.json (an empty folder there follows Obsidian's new-note location), falling back to \"Daily Notes\" when that file is missing.", "default": "Daily Notes" }, { @@ -160,11 +160,11 @@ }, { "name": "PROTECTED_PATHS", - "description": "Comma-separated vault folder names blocked from vault_delete_note and vault_move_note. Default: MEMORY_DIR plus the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json (default Daily Notes). When set, overrides the default entirely." + "description": "Comma-separated vault folder names blocked from vault_delete_note and vault_move_note. Default: MEMORY_DIR plus the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json (default Daily Notes; none when that setting is the vault root or empty). When set, overrides the default entirely." }, { "name": "ORPHAN_EXCLUDE_FOLDERS", - "description": "Comma-separated vault folder names excluded from vault_find_orphans. Default: the daily notes folder, \"Templates\", and MEMORY_DIR. DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is read per query (fallback \"Daily Notes\"). When set, replaces the defaults entirely. Set to a comma (,) to exclude nothing; an empty value uses the defaults." + "description": "Comma-separated vault folder names excluded from vault_find_orphans. Default: the daily notes folder, \"Templates\", and MEMORY_DIR. DAILY_NOTES_FOLDER wins; otherwise .obsidian/daily-notes.json is read per query (fallback \"Daily Notes\"; no daily folder when that setting is the vault root or empty). When set, replaces the defaults entirely. Set to a comma (,) to exclude nothing; an empty value uses the defaults." }, { "name": "SERVICE_DOCUMENTATION_URL", From 71bcc1100bf9b66c1ac43b680e7024644db73067 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:07:12 -0400 Subject: [PATCH 15/26] feat(daily-notes): accept / in DAILY_NOTES_FOLDER as the vault root Obsidian saves the vault root as "/" in daily-notes.json; DAILY_NOTES_FOLDER now takes the same value instead of refusing it at boot. The root is a shared folder, so the delete/move guard and the orphan exclusions add no daily folder for it. Co-Authored-By: Claude Opus 5.5 --- .env.example | 5 +++-- DEPLOY.md | 2 +- README.md | 2 +- cli/src/env.ts | 7 +++--- deploy/local/.env.example | 2 +- deploy/local/docker-compose.yml | 2 +- deploy/remote/.env.example | 5 +++-- deploy/remote/README.md | 2 +- deploy/remote/docker-compose.yml | 2 +- docker-compose.local.yml | 2 +- docker-compose.yml | 2 +- server.json | 2 +- src/vault-mcp/__tests__/config.test.ts | 10 +++++++++ src/vault-mcp/config.ts | 14 ++++++++++-- .../__tests__/daily-notes.test.ts | 16 ++++++++++++++ .../__tests__/vault-folder-config.test.ts | 13 +++++++++++ src/vault-mcp/vault-operations/daily-notes.ts | 17 +++++++++----- .../vault-operations/vault-folder-config.ts | 22 ++++++++++++------- 18 files changed, 95 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 286ec23eb..8a01e7652 100644 --- a/.env.example +++ b/.env.example @@ -225,8 +225,9 @@ GHCR_USER= # "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An # empty folder in that file means Obsidian's location for new notes, which # syncs only when SYNC_CONFIGS includes app; without it, the server uses the -# vault root. Folder is any vault-relative path (Journal, Planner/Daily); -# format takes the same tokens as Obsidian's date format setting. +# vault root. Folder is any vault-relative path (Journal, Planner/Daily), or +# / for the vault root; format takes the same tokens as Obsidian's date format +# setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD # Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus diff --git a/DEPLOY.md b/DEPLOY.md index 5f3252b2e..2e1b08ed0 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -370,7 +370,7 @@ To find your stage: `cat .sst/stage` (after one-time setup). | `SYNC_CONFIGS` | Optional. Obsidian settings categories synced to the server, comma-separated (default: `core-plugin-data,community-plugin-data` — daily-notes settings and the Tasks plugin's format). Set `none` to disable. See [Daily notes](./README.md#daily-notes). | | `SYNC_EXCLUDED_FOLDERS` | Optional. Folders to leave out of Obsidian Sync, comma-separated — the same list as Obsidian's Sync → Excluded folders. Unset syncs everything. | | `SYNC_FILE_TYPES` | Optional. Attachment types Obsidian Sync delivers, comma-separated. **Valid values:** `image`, `audio`, `video`, `pdf`, `unsupported`. **Unset (default):** `image,audio,video,pdf`. The text files Vault Cortex reads (CSV, JSON, TXT, XML, LOG, YAML) fall under `unsupported`, Obsidian Sync's name for its **Sync all other types** category; SVG syncs as `image`, and notes, canvases, and bases always sync. To read those text files, set it to `image,audio,video,pdf,unsupported` (the value replaces the default list, so keep the four defaults) and turn on **Sync all other types** in Obsidian's Sync settings on the device that has them. An invalid value is skipped with a warning in the logs, and the default types sync instead. | -| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder (default: read from the vault's `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable). See [Daily notes](./README.md#daily-notes). | +| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder (default: read from the vault's `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable; `/` names the vault root). See [Daily notes](./README.md#daily-notes). | | `DAILY_NOTES_FORMAT` | Optional. Sets the daily note filename format, in the same tokens as Obsidian's daily note date format setting (default: read from the vault's config, falling back to `YYYY-MM-DD`). | | `TZ` | Optional. Container timezone (default: `UTC`). Affects `vault_update_memory` date stamps and `vault_get_daily_note` date resolution. Set to your IANA timezone (e.g. `America/New_York`). | | `LOG_LEVEL` | Optional. Logging verbosity: `debug`, `info`, `warn`, `error`. Default: `info`. | diff --git a/README.md b/README.md index a74322f01..a7d209dc5 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,7 @@ See [`templates/memory/`](./templates/memory/) for memory file examples and the When the file isn't available — or if you use the Periodic Notes plugin, whose settings `daily-notes.json` doesn't hold — set the values yourself: -- `DAILY_NOTES_FOLDER` — any vault-relative path: `Journal`, `Planner/Daily` +- `DAILY_NOTES_FOLDER` — any vault-relative path: `Journal`, `Planner/Daily`, or `/` for the vault root - `DAILY_NOTES_FORMAT` — same tokens as Obsidian's date format setting: `YYYY-MM-DD-dddd`, `YYYY/MM/DD`, `MMM D, YYYY`, … You can set one or both — a set value always wins over the config file. A value you leave unset comes from `daily-notes.json`; when that file is missing, the server uses `Daily Notes` and `YYYY-MM-DD`. diff --git a/cli/src/env.ts b/cli/src/env.ts index cb6240d34..2df5256eb 100644 --- a/cli/src/env.ts +++ b/cli/src/env.ts @@ -98,7 +98,7 @@ MEMORY_DIR=About Me # .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file # is missing or unreadable. An empty folder in that file means Obsidian's # location for new notes. Folder is any vault-relative path (Journal, -# Planner/Daily); format takes the same tokens as Obsidian's date format setting. +# Planner/Daily), or / for the vault root; format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD @@ -250,8 +250,9 @@ MEMORY_DIR=About Me # "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An # empty folder in that file means Obsidian's location for new notes, which # syncs only when SYNC_CONFIGS includes app; without it, the server uses the -# vault root. Folder is any vault-relative path (Journal, Planner/Daily); -# format takes the same tokens as Obsidian's date format setting. +# vault root. Folder is any vault-relative path (Journal, Planner/Daily), or +# / for the vault root; format takes the same tokens as Obsidian's date format +# setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD diff --git a/deploy/local/.env.example b/deploy/local/.env.example index 87ce2d0b8..292909623 100644 --- a/deploy/local/.env.example +++ b/deploy/local/.env.example @@ -88,7 +88,7 @@ MEMORY_DIR=About Me # .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file # is missing or unreadable. An empty folder in that file means Obsidian's # location for new notes. Folder is any vault-relative path (Journal, -# Planner/Daily); format takes the same tokens as Obsidian's date format setting. +# Planner/Daily), or / for the vault root; format takes the same tokens as Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD diff --git a/deploy/local/docker-compose.yml b/deploy/local/docker-compose.yml index bec60c75b..5d7020a8d 100644 --- a/deploy/local/docker-compose.yml +++ b/deploy/local/docker-compose.yml @@ -57,7 +57,7 @@ services: # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder - # there follows Obsidian's new-note location), else "Daily Notes" + # there follows Obsidian's new-note location), else "Daily Notes"; set / for the vault root # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/deploy/remote/.env.example b/deploy/remote/.env.example index baffaf5f1..f7521b69e 100644 --- a/deploy/remote/.env.example +++ b/deploy/remote/.env.example @@ -94,8 +94,9 @@ MEMORY_DIR=About Me # "Daily Notes" and YYYY-MM-DD when that file is missing or unreadable. An # empty folder in that file means Obsidian's location for new notes, which # syncs only when SYNC_CONFIGS includes app; without it, the server uses the -# vault root. Folder is any vault-relative path (Journal, Planner/Daily); -# format takes the same tokens as Obsidian's date format setting. +# vault root. Folder is any vault-relative path (Journal, Planner/Daily), or +# / for the vault root; format takes the same tokens as Obsidian's date format +# setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 78604eb94..7c6471d95 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -577,7 +577,7 @@ explains why. If the settings can't sync — or you use the Periodic Notes plugin, whose settings the core config file doesn't track — set `DAILY_NOTES_FOLDER` (any -vault-relative path: `Journal`, `Planner/Daily`) and `DAILY_NOTES_FORMAT` +vault-relative path: `Journal`, `Planner/Daily`, or `/` for the vault root) and `DAILY_NOTES_FORMAT` (same tokens as Obsidian's date format setting) in `.env`. You can set one or both — a set value always wins over the config file. A value you leave unset while `daily-notes.json` hasn't reached the server comes from the diff --git a/deploy/remote/docker-compose.yml b/deploy/remote/docker-compose.yml index 7591b8ce5..e13d1e649 100644 --- a/deploy/remote/docker-compose.yml +++ b/deploy/remote/docker-compose.yml @@ -73,7 +73,7 @@ services: # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder - # there follows Obsidian's new-note location), else "Daily Notes" + # there follows Obsidian's new-note location), else "Daily Notes"; set / for the vault root # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 14abc1727..b464a7bdf 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -55,7 +55,7 @@ services: # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder - # there follows Obsidian's new-note location), else "Daily Notes" + # there follows Obsidian's new-note location), else "Daily Notes"; set / for the vault root # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/docker-compose.yml b/docker-compose.yml index 3251bd444..b219bf403 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: # (neither default adds a daily folder when the daily notes folder setting is the vault root or empty) # SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex # DAILY_NOTES_FOLDER default: read from the vault's .obsidian/daily-notes.json (an empty folder - # there follows Obsidian's new-note location), else "Daily Notes" + # there follows Obsidian's new-note location), else "Daily Notes"; set / for the vault root # DAILY_NOTES_FORMAT default: read from the vault's .obsidian/daily-notes.json, else "YYYY-MM-DD" PROTECTED_PATHS: ${PROTECTED_PATHS:-} ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-} diff --git a/server.json b/server.json index c6d3fb78f..62d820ae3 100644 --- a/server.json +++ b/server.json @@ -118,7 +118,7 @@ }, { "name": "DAILY_NOTES_FOLDER", - "description": "Vault folder for daily notes. Overrides the folder configured in Obsidian's daily-notes plugin. When unset, read from the vault's .obsidian/daily-notes.json (an empty folder there follows Obsidian's new-note location), falling back to \"Daily Notes\" when that file is missing.", + "description": "Vault folder for daily notes. Overrides the folder configured in Obsidian's daily-notes plugin. When unset, read from the vault's .obsidian/daily-notes.json (an empty folder there follows Obsidian's new-note location), falling back to \"Daily Notes\" when that file is missing. Set to / for the vault root.", "default": "Daily Notes" }, { diff --git a/src/vault-mcp/__tests__/config.test.ts b/src/vault-mcp/__tests__/config.test.ts index 517c60c5b..2577ed7d4 100644 --- a/src/vault-mcp/__tests__/config.test.ts +++ b/src/vault-mcp/__tests__/config.test.ts @@ -159,6 +159,16 @@ describe("loadConfig", () => { "absolute paths not allowed", ) }) + + it.each([ + { label: "a lone slash", input: "/" }, + { label: "a slash with surrounding spaces", input: " / " }, + { label: "repeated slashes", input: "//" }, + { label: "a backslash", input: "\\" }, + ])("reads $label as the vault root", ({ input }) => { + const config = loadConfig({ DAILY_NOTES_FOLDER: input }) + expect(config.dailyNotesFolder).toBe("") + }) }) describe("DAILY_NOTES_FORMAT", () => { diff --git a/src/vault-mcp/config.ts b/src/vault-mcp/config.ts index b5b2d2076..ede5bf145 100644 --- a/src/vault-mcp/config.ts +++ b/src/vault-mcp/config.ts @@ -25,6 +25,15 @@ const vaultFolderName = z .refine((value) => !value.startsWith("/"), "absolute paths not allowed"), ) +/** Matches a value made only of slashes, which Obsidian reads as the vault root. */ +const VAULT_ROOT_FOLDER_RE = /^[\\/]+$/ + +/** Parses DAILY_NOTES_FOLDER: "/" is the vault root (""), the value Obsidian + * saves for it; anything else must be a vault folder name. */ +const parseDailyNotesFolder = (folder: string): string => { + return VAULT_ROOT_FOLDER_RE.test(folder) ? "" : vaultFolderName.parse(folder) +} + /** Splits a comma-separated env value into its entries. * Trims each entry; empty entries (from trailing commas) are filtered out. */ const splitCommaSeparatedValues = (raw: string): string[] => @@ -112,7 +121,8 @@ export type VaultConfig = Readonly<{ /** Sets the daily notes folder, taking precedence over * .obsidian/daily-notes.json. * Per-field precedence: env setting → daily-notes.json → fallback - * ("Daily Notes"). Set via DAILY_NOTES_FOLDER. */ + * ("Daily Notes"). Set via DAILY_NOTES_FOLDER; "/" (Obsidian's spelling of + * the vault root) loads as "", so a set value is any defined one. */ dailyNotesFolder?: string | undefined /** Sets the daily notes filename format (moment tokens, matching * Obsidian's setting), taking precedence over .obsidian/daily-notes.json. @@ -174,7 +184,7 @@ export const loadConfig = (env: Record = process.env const dailyNotesFolderRaw = env.DAILY_NOTES_FOLDER?.trim() const dailyNotesFolder = dailyNotesFolderRaw - ? vaultFolderName.parse(dailyNotesFolderRaw) + ? parseDailyNotesFolder(dailyNotesFolderRaw) : undefined const dailyNotesFormatRaw = env.DAILY_NOTES_FORMAT?.trim() diff --git a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts index 0c05202c0..580c8c393 100644 --- a/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/daily-notes.test.ts @@ -390,6 +390,22 @@ describe("readDailyNotesConfig", () => { }) }) + it.each([ + { label: "alone", envSettings: { folder: "" } }, + { label: "with a format", envSettings: { folder: "", format: "DD-MM-YYYY" } }, + ])( + "treats a vault-root folder override ($label) as the shared root", + async ({ envSettings }) => { + const vaultDir = await createVault() + // A named folder on disk, so falling through to the file would show + await writeDailyNotesConfig(vaultDir, { folder: "Journal", format: "DD-MM-YYYY" }) + + const config = await readDailyNotesConfig({ vaultPath: vaultDir, envSettings }, logger) + + expect(config).toEqual({ folder: "", format: "DD-MM-YYYY", dedicatedFolder: null }) + }, + ) + it("folder-only override without a config file falls back to the default format", async () => { const vaultDir = await createVault() diff --git a/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts b/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts index 798cd6387..8275bee28 100644 --- a/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/vault-folder-config.test.ts @@ -114,6 +114,14 @@ describe("resolveEffectiveProtectedPaths", () => { expect(requestLogger.warn).not.toHaveBeenCalled() }) + it("protects only memory for DAILY_NOTES_FOLDER=/ without reading the file", async () => { + const vaultPath = await createVault('{"folder":"Journal"}') + const config = loadConfig({ DAILY_NOTES_FOLDER: "/" }) + expect(await resolveEffectiveProtectedPaths({ config, vaultPath }, logger)).toEqual([ + "About Me", + ]) + }) + it("reads the file folder when only DAILY_NOTES_FORMAT is set", async () => { const vaultPath = await createVault('{"folder":"Journal"}') const config = loadConfig({ DAILY_NOTES_FORMAT: "DD-MM-YYYY" }) @@ -240,6 +248,11 @@ describe("readEffectiveOrphanExcludeFolders", () => { env: { DAILY_NOTES_FOLDER: "Env/Journal" }, expected: ["Env/Journal", "Templates", "About Me"], }, + { + label: "vault-root daily folder override", + env: { DAILY_NOTES_FOLDER: "/" }, + expected: ["Templates", "About Me"], + }, { label: "explicit orphan override", env: { ORPHAN_EXCLUDE_FOLDERS: "Archive,Scratch" }, diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index 5d8c7a184..195528071 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -45,7 +45,8 @@ type DailyNotesFileSettings = { } /** Per-field settings from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env - * vars; each set field takes precedence over .obsidian/daily-notes.json. */ + * vars; each set field takes precedence over .obsidian/daily-notes.json. + * A folder of "" is the vault root. */ export type DailyNotesEnvSettings = { folder?: string | undefined format?: string | undefined @@ -244,9 +245,14 @@ export const readDailyNotesConfig = async ( const envFolder = envSettings?.folder const envFormat = envSettings?.format + // DAILY_NOTES_FOLDER="/" arrives as "" (the vault root), so a set folder is + // any defined value, and the root is shared rather than dedicated. + const hasEnvFolder = envFolder !== undefined + const envDedicatedFolder = envFolder || null + // Both fields set — the files can't contribute anything, skip I/O. - if (envFolder && envFormat) { - return { folder: envFolder, format: envFormat, dedicatedFolder: envFolder } + if (hasEnvFolder && envFormat) { + return { folder: envFolder, format: envFormat, dedicatedFolder: envDedicatedFolder } } const { folderSetting, format: fileFormat } = await readDailyNotesFileOrFallback( @@ -255,9 +261,8 @@ export const readDailyNotesConfig = async ( ) const format = envFormat ?? fileFormat - // loadConfig trims DAILY_NOTES_FOLDER, so a set value is never blank. - if (envFolder) { - return { folder: envFolder, format, dedicatedFolder: envFolder } + if (hasEnvFolder) { + return { folder: envFolder, format, dedicatedFolder: envDedicatedFolder } } const folder = await resolveDailyNotesFolder({ folderSetting, vaultPath }, logger) diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index 0284dd237..489b50906 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -15,9 +15,10 @@ const pickDailyFolderToAdd = (dailyFolder: string | null): string | null => { * plus the daily notes folder, resolved on each call (DAILY_NOTES_FOLDER → * .obsidian/daily-notes.json → "Daily Notes") so a folder configured only * in the vault is protected too. - * - A daily-notes.json folder that is the vault root or blank (Obsidian's - * new-note folder) protects no daily folder, since protecting either - * would cover every other note in it. + * - A folder setting that is the vault root (in DAILY_NOTES_FOLDER or + * daily-notes.json) or blank in daily-notes.json (Obsidian's new-note + * folder) protects no daily folder, since protecting either would cover + * every other note in it. * Throws when daily-notes.json exists but cannot be read, since the folder * it names is then unknown; DAILY_NOTES_FOLDER or PROTECTED_PATHS bypasses * the file. */ @@ -27,8 +28,13 @@ export const resolveEffectiveProtectedPaths = async ( ): Promise => { if (config.protectedPathsOverride) return config.protectedPathsOverride - // loadConfig trims DAILY_NOTES_FOLDER, so a set value is never blank. - if (config.dailyNotesFolder) return [config.memoryDir, config.dailyNotesFolder] + // DAILY_NOTES_FOLDER="/" loads as "" (the vault root), which protects no + // daily folder; any other set value is never blank. + if (config.dailyNotesFolder !== undefined) { + return config.dailyNotesFolder + ? [config.memoryDir, config.dailyNotesFolder] + : [config.memoryDir] + } const protectedDailyFolder = await readProtectedDailyNotesFolder(vaultPath, logger) const dailyFolder = pickDailyFolderToAdd(protectedDailyFolder) @@ -65,12 +71,12 @@ export const readEffectiveOrphanExcludeFolders = async ( if (config.orphanExcludeFoldersOverride) return config.orphanExcludeFoldersOverride // The daily folder is the only default that varies, so an env value for - // it means no file read. - if (config.dailyNotesFolder) { + // it means no file read. The vault root ("") adds no daily folder. + if (config.dailyNotesFolder !== undefined) { return resolveEffectiveOrphanExcludeFolders({ orphanExcludeFoldersOverride: null, memoryDir: config.memoryDir, - dailyNotesFolder: config.dailyNotesFolder, + dailyNotesFolder: config.dailyNotesFolder || null, }) } From ad26904d808435eacbd26f9b4179a5a5d41b2183 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:12:54 -0400 Subject: [PATCH 16/26] style(daily-notes): clarify vault-root folder comments and rewrap doc lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.ts: name both slash directions in VAULT_ROOT_FOLDER_RE's doc, attach "the value Obsidian saves" to "/" rather than "", and state the `!== undefined` consequence on VaultConfig.dailyNotesFolder. - daily-notes.ts, vault-folder-config.ts: say plainly why a "" env folder is set but not dedicated, and why other set values skip the whitespace check; split the protected-paths bullet's nested parentheticals. - DEPLOY.md: move "/" out of the default parenthetical into the value shape. - deploy/local/.env.example (synced to cli/src/env.ts), deploy/remote/README.md: rewrap overlong lines. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- DEPLOY.md | 2 +- cli/src/env.ts | 3 ++- deploy/local/.env.example | 3 ++- deploy/remote/README.md | 10 +++++----- src/vault-mcp/config.ts | 11 ++++++----- src/vault-mcp/vault-operations/daily-notes.ts | 5 +++-- src/vault-mcp/vault-operations/vault-folder-config.ts | 11 ++++++----- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 2e1b08ed0..b6596f870 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -370,7 +370,7 @@ To find your stage: `cat .sst/stage` (after one-time setup). | `SYNC_CONFIGS` | Optional. Obsidian settings categories synced to the server, comma-separated (default: `core-plugin-data,community-plugin-data` — daily-notes settings and the Tasks plugin's format). Set `none` to disable. See [Daily notes](./README.md#daily-notes). | | `SYNC_EXCLUDED_FOLDERS` | Optional. Folders to leave out of Obsidian Sync, comma-separated — the same list as Obsidian's Sync → Excluded folders. Unset syncs everything. | | `SYNC_FILE_TYPES` | Optional. Attachment types Obsidian Sync delivers, comma-separated. **Valid values:** `image`, `audio`, `video`, `pdf`, `unsupported`. **Unset (default):** `image,audio,video,pdf`. The text files Vault Cortex reads (CSV, JSON, TXT, XML, LOG, YAML) fall under `unsupported`, Obsidian Sync's name for its **Sync all other types** category; SVG syncs as `image`, and notes, canvases, and bases always sync. To read those text files, set it to `image,audio,video,pdf,unsupported` (the value replaces the default list, so keep the four defaults) and turn on **Sync all other types** in Obsidian's Sync settings on the device that has them. An invalid value is skipped with a warning in the logs, and the default types sync instead. | -| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder (default: read from the vault's `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable; `/` names the vault root). See [Daily notes](./README.md#daily-notes). | +| `DAILY_NOTES_FOLDER` | Optional. Sets the daily notes folder: any vault-relative path, or `/` for the vault root (default: read from the vault's `.obsidian/daily-notes.json`; `Daily Notes` when that file is missing or unreadable). See [Daily notes](./README.md#daily-notes). | | `DAILY_NOTES_FORMAT` | Optional. Sets the daily note filename format, in the same tokens as Obsidian's daily note date format setting (default: read from the vault's config, falling back to `YYYY-MM-DD`). | | `TZ` | Optional. Container timezone (default: `UTC`). Affects `vault_update_memory` date stamps and `vault_get_daily_note` date resolution. Set to your IANA timezone (e.g. `America/New_York`). | | `LOG_LEVEL` | Optional. Logging verbosity: `debug`, `info`, `warn`, `error`. Default: `info`. | diff --git a/cli/src/env.ts b/cli/src/env.ts index 2df5256eb..7cab4035c 100644 --- a/cli/src/env.ts +++ b/cli/src/env.ts @@ -98,7 +98,8 @@ MEMORY_DIR=About Me # .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file # is missing or unreadable. An empty folder in that file means Obsidian's # location for new notes. Folder is any vault-relative path (Journal, -# Planner/Daily), or / for the vault root; format takes the same tokens as Obsidian's date format setting. +# Planner/Daily), or / for the vault root; format takes the same tokens as +# Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD diff --git a/deploy/local/.env.example b/deploy/local/.env.example index 292909623..14abdb262 100644 --- a/deploy/local/.env.example +++ b/deploy/local/.env.example @@ -88,7 +88,8 @@ MEMORY_DIR=About Me # .obsidian/daily-notes.json, or "Daily Notes" and YYYY-MM-DD when that file # is missing or unreadable. An empty folder in that file means Obsidian's # location for new notes. Folder is any vault-relative path (Journal, -# Planner/Daily), or / for the vault root; format takes the same tokens as Obsidian's date format setting. +# Planner/Daily), or / for the vault root; format takes the same tokens as +# Obsidian's date format setting. # DAILY_NOTES_FOLDER=Journal # DAILY_NOTES_FORMAT=YYYY-MM-DD diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 7c6471d95..aaf62bc94 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -577,11 +577,11 @@ explains why. If the settings can't sync — or you use the Periodic Notes plugin, whose settings the core config file doesn't track — set `DAILY_NOTES_FOLDER` (any -vault-relative path: `Journal`, `Planner/Daily`, or `/` for the vault root) and `DAILY_NOTES_FORMAT` -(same tokens as Obsidian's date format setting) in `.env`. You can set one -or both — a set value always wins over the config file. A value you leave -unset while `daily-notes.json` hasn't reached the server comes from the -fallbacks: the `Daily Notes` folder and `YYYY-MM-DD`. +vault-relative path: `Journal`, `Planner/Daily`, or `/` for the vault root) +and `DAILY_NOTES_FORMAT` (same tokens as Obsidian's date format setting) in +`.env`. You can set one or both — a set value always wins over the config +file. A value you leave unset while `daily-notes.json` hasn't reached the +server comes from the fallbacks: the `Daily Notes` folder and `YYYY-MM-DD`. `.env` edits take effect when the container is re-created: set up with the CLI, run `npx vault-cortex@latest restart`; with Compose, `docker compose up -d`; diff --git a/src/vault-mcp/config.ts b/src/vault-mcp/config.ts index ede5bf145..f88070438 100644 --- a/src/vault-mcp/config.ts +++ b/src/vault-mcp/config.ts @@ -25,11 +25,12 @@ const vaultFolderName = z .refine((value) => !value.startsWith("/"), "absolute paths not allowed"), ) -/** Matches a value made only of slashes, which Obsidian reads as the vault root. */ +/** Matches a value made only of forward or back slashes, which Obsidian reads + * as the vault root. */ const VAULT_ROOT_FOLDER_RE = /^[\\/]+$/ -/** Parses DAILY_NOTES_FOLDER: "/" is the vault root (""), the value Obsidian - * saves for it; anything else must be a vault folder name. */ +/** Parses DAILY_NOTES_FOLDER. "/", the value Obsidian saves for the vault + * root, loads as ""; anything else must be a vault folder name. */ const parseDailyNotesFolder = (folder: string): string => { return VAULT_ROOT_FOLDER_RE.test(folder) ? "" : vaultFolderName.parse(folder) } @@ -121,8 +122,8 @@ export type VaultConfig = Readonly<{ /** Sets the daily notes folder, taking precedence over * .obsidian/daily-notes.json. * Per-field precedence: env setting → daily-notes.json → fallback - * ("Daily Notes"). Set via DAILY_NOTES_FOLDER; "/" (Obsidian's spelling of - * the vault root) loads as "", so a set value is any defined one. */ + * ("Daily Notes"). Set via DAILY_NOTES_FOLDER; "/" (the vault root) loads + * as "", so test `!== undefined`, not truthiness, for a set value. */ dailyNotesFolder?: string | undefined /** Sets the daily notes filename format (moment tokens, matching * Obsidian's setting), taking precedence over .obsidian/daily-notes.json. diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index 195528071..a3f96dacf 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -245,8 +245,9 @@ export const readDailyNotesConfig = async ( const envFolder = envSettings?.folder const envFormat = envSettings?.format - // DAILY_NOTES_FOLDER="/" arrives as "" (the vault root), so a set folder is - // any defined value, and the root is shared rather than dedicated. + // DAILY_NOTES_FOLDER="/" arrives as "" (the vault root). That is still a set + // folder, so test for undefined; its dedicatedFolder is null, as for a + // vault-root setting in daily-notes.json. const hasEnvFolder = envFolder !== undefined const envDedicatedFolder = envFolder || null diff --git a/src/vault-mcp/vault-operations/vault-folder-config.ts b/src/vault-mcp/vault-operations/vault-folder-config.ts index 489b50906..a28307291 100644 --- a/src/vault-mcp/vault-operations/vault-folder-config.ts +++ b/src/vault-mcp/vault-operations/vault-folder-config.ts @@ -15,10 +15,10 @@ const pickDailyFolderToAdd = (dailyFolder: string | null): string | null => { * plus the daily notes folder, resolved on each call (DAILY_NOTES_FOLDER → * .obsidian/daily-notes.json → "Daily Notes") so a folder configured only * in the vault is protected too. - * - A folder setting that is the vault root (in DAILY_NOTES_FOLDER or - * daily-notes.json) or blank in daily-notes.json (Obsidian's new-note - * folder) protects no daily folder, since protecting either would cover - * every other note in it. + * - No daily folder is protected when the setting is the vault root, from + * DAILY_NOTES_FOLDER or daily-notes.json, or is blank in daily-notes.json, + * which means Obsidian's new-note folder. Protecting either folder would + * cover every other note in it. * Throws when daily-notes.json exists but cannot be read, since the folder * it names is then unknown; DAILY_NOTES_FOLDER or PROTECTED_PATHS bypasses * the file. */ @@ -29,7 +29,8 @@ export const resolveEffectiveProtectedPaths = async ( if (config.protectedPathsOverride) return config.protectedPathsOverride // DAILY_NOTES_FOLDER="/" loads as "" (the vault root), which protects no - // daily folder; any other set value is never blank. + // daily folder. loadConfig trims and rejects blank values, so any other set + // value needs no pickDailyFolderToAdd check. if (config.dailyNotesFolder !== undefined) { return config.dailyNotesFolder ? [config.memoryDir, config.dailyNotesFolder] From 6384c8287fec82816b21d9733a721d2c1717db16 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:14:40 -0400 Subject: [PATCH 17/26] fix(cli): accept / as the vault root in the daily notes folder prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guided settings flow (init and configure) rejected "/" for DAILY_NOTES_FOLDER as an absolute path, while the server now loads it as the vault root and the .env comments the CLI writes document it. A value made only of slashes now passes the prompt's validation and is written as entered. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- cli/src/__tests__/optional-settings.test.ts | 9 +++++++++ cli/src/optional-settings.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/cli/src/__tests__/optional-settings.test.ts b/cli/src/__tests__/optional-settings.test.ts index 7f53ab747..37ee0a0d2 100644 --- a/cli/src/__tests__/optional-settings.test.ts +++ b/cli/src/__tests__/optional-settings.test.ts @@ -611,6 +611,15 @@ describe("DAILY_NOTES_FOLDER validate callback", () => { ]) expect(overrides).toEqual({ DAILY_NOTES_FOLDER: "Journal" }) }) + + it("accepts / as the vault root", async () => { + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "/"]) + + const overrides = await askOptionalSettings({ mode: "local", envContent: "" }, scripted.prompts) + + expect(scripted.errors).toEqual([]) + expect(overrides).toEqual({ DAILY_NOTES_FOLDER: "/" }) + }) }) describe("DAILY_NOTES_FORMAT validate callback", () => { diff --git a/cli/src/optional-settings.ts b/cli/src/optional-settings.ts index 2b9a1f0cc..308d77832 100644 --- a/cli/src/optional-settings.ts +++ b/cli/src/optional-settings.ts @@ -55,6 +55,10 @@ type OptionalSetting = /** Matches Moment.js [...] literal escape groups, splitting format spans from literal content. */ const MOMENT_BRACKET_ESCAPE = /\[([^\]]*)\]/g +/** Matches a value made only of forward or back slashes, which the server + * reads as the vault root. */ +const VAULT_ROOT_FOLDER_RE = /^[\\/]+$/ + // The curated prompt set — settings users most often want without reading // .env comments. Everything else stays documented-only in the generated // optional block, deliberately: every extra prompt costs init flow length. @@ -80,6 +84,8 @@ const OPTIONAL_SETTINGS: OptionalSetting[] = [ question: "Vault folder for daily notes:", placeholder: "blank = use your vault's daily notes settings", validate: (value) => { + // "/" names the vault root, so the absolute-path check below must not catch it. + if (VAULT_ROOT_FOLDER_RE.test(value)) return undefined if (value.includes("..")) return "Path traversal (..) is not allowed in folder names." if (value.startsWith("/")) return "Absolute paths are not allowed — use a vault-relative folder name." From 17651c6a8da40235da40caafa3d90923c2f7f8d9 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:17:03 -0400 Subject: [PATCH 18/26] docs(daily-notes): scope the folder normalization note to daily-notes.json Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e81e338e1..1e3a16c6f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -246,7 +246,7 @@ Every tag input (`tag` on `vault_search_by_tag` and `vault_list_tasks`, the `tag **Daily notes:** `vault_get_daily_note` resolves the vault's folder and date format, each independently: `DAILY_NOTES_FOLDER`/`DAILY_NOTES_FORMAT` env setting → `.obsidian/daily-notes.json` → fallback (`Daily Notes/YYYY-MM-DD.md`). - **Read on every call:** a folder or format change in Obsidian applies to the next call, and a config file that arrives after boot needs no restart -- **Folder normalized like Obsidian:** runs of `/` or `\` become one `/` and leading and trailing slashes are dropped, so Obsidian's `"/"` is the vault root; no-break spaces become plain spaces and the name is NFC-normalized +- **File folder normalized like Obsidian:** in `daily-notes.json`, runs of `/` or `\` become one `/` and leading and trailing slashes are dropped, so Obsidian's `"/"` is the vault root; no-break spaces become plain spaces and the name is NFC-normalized. `DAILY_NOTES_FOLDER` takes `/` for the vault root and is otherwise validated as a vault folder name - **Blank folder in the file:** Obsidian's new-note location (`newFileLocation: "folder"` plus `newFileFolderPath` in `.obsidian/app.json`), otherwise the vault root - **Missing file:** takes the fallbacks - **Unparseable file:** takes the fallbacks and logs a `warn` From cee88ee11e8c9dc2c0196848012d7087913b8c6d Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:53:02 -0400 Subject: [PATCH 19/26] fix(daily-notes): finish the tool text for root and blank folder settings vault_get_daily_note states the new-note default and lists the path-blocked errors a configured folder can raise; vault_delete_note and vault_move_note list the filesystem errors; the orphan hint and forward-ref exception name the folder setting and cover the vault root. Co-Authored-By: Claude Opus 5.5 --- .../__tests__/__snapshots__/tool-surface/default.json | 10 +++++----- .../__snapshots__/tool-surface/disabled-tools.json | 10 +++++----- .../__snapshots__/tool-surface/embedding-off.json | 10 +++++----- .../tool-surface/file-tools-off+embedding-off.json | 10 +++++----- .../__snapshots__/tool-surface/file-tools-off.json | 10 +++++----- .../tool-surface/memory-off+embedding-off.json | 10 +++++----- .../memory-off+file-tools-off+embedding-off.json | 10 +++++----- .../tool-surface/memory-off+file-tools-off.json | 10 +++++----- .../__snapshots__/tool-surface/memory-off.json | 10 +++++----- .../__snapshots__/tool-surface/obsidian-sync.json | 10 +++++----- .../tool-surface/readonly+embedding-off.json | 6 +++--- .../readonly+file-tools-off+embedding-off.json | 6 +++--- .../tool-surface/readonly+file-tools-off.json | 6 +++--- .../readonly+memory-off+embedding-off.json | 6 +++--- ...adonly+memory-off+file-tools-off+embedding-off.json | 6 +++--- .../readonly+memory-off+file-tools-off.json | 6 +++--- .../tool-surface/readonly+memory-off.json | 6 +++--- .../__tests__/__snapshots__/tool-surface/readonly.json | 6 +++--- .../mcp-core/__tests__/tool-definitions.test.ts | 8 +++++++- .../mcp-core/__tests__/tool-surface-snapshot.test.ts | 2 +- src/vault-mcp/mcp-core/tools/daily-note-tools.ts | 3 ++- src/vault-mcp/mcp-core/tools/search-tools.ts | 4 ++-- src/vault-mcp/mcp-core/tools/vault-crud-tools.ts | 4 +++- 23 files changed, 89 insertions(+), 80 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 1edfd1195..8a2581dcb 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 @@ -194,7 +194,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -334,7 +334,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -394,7 +394,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 41778775a..3fa3d9bce 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 fb489aa01..bc65ccdb7 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 7ae2bf3c1..71adc8d28 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 @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -337,7 +337,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -397,7 +397,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 66ad7b5c5..29bdc7ebe 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 a24a3db82..f2a734229 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 e638c570b..e4a5a3a04 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 @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -292,7 +292,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -317,7 +317,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 25708e3c4..4d6f7e72e 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -316,7 +316,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 6bc8a2ed6..ea0b3e433 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 @@ -150,7 +150,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -290,7 +290,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -315,7 +315,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 c7adf42fe..9123eef29 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -396,7 +396,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 e629ac7c0..094c55fe3 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 8d01a5d32..d134e335f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -133,7 +133,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 137e615cc..dd794ea9b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -132,7 +132,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 5823a8af8..70daaab12 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 1e6f482d6..c990e4c57 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -99,7 +99,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 88c4af976..0828da880 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -98,7 +98,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 3cc2bf0bb..171819c7b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -97,7 +97,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "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 ce321aa0a..b8765c5d3 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -71,7 +71,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { @@ -131,7 +131,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"). When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken (kind is always \"note\"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 160a4ebfc..43b9ee4e1 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -1117,6 +1117,10 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { '- "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry' const DAILY_NOTES_CONFIG_ERROR_ENTRY = '- "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry' + const FILESYSTEM_ERROR_ENTRY_LINES = [ + '- "ENAMETOOLONG: …" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in ".tmp", the note exists but its name is too long to save; rename it at least 41 bytes shorter', + "- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it", + ] it("lists the trash-move and trash-setting errors when the server does not sync", () => { expect(deleteNoteErrors({})).toBe( @@ -1126,6 +1130,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { '- "cannot move to trash … — 100 collisions in .trash/" — .trash/ holds this name and 100 numbered copies ("Plan 1.md" … "Plan 100.md"); ask the vault owner to clear old copies, then retry', '- any other "cannot move to trash …" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry', OTHER_DELETE_ERROR_ENTRY, + ...FILESYSTEM_ERROR_ENTRY_LINES, "- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry", DAILY_NOTES_CONFIG_ERROR_ENTRY, ].join("\n"), @@ -1138,6 +1143,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { "Errors:", ...PATH_AND_LOOKUP_ERROR_ENTRIES, OTHER_DELETE_ERROR_ENTRY, + ...FILESYSTEM_ERROR_ENTRY_LINES, DAILY_NOTES_CONFIG_ERROR_ENTRY, ].join("\n"), ) @@ -2712,7 +2718,7 @@ describe("DISABLED_TOOLS", () => { '. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank)${ENTRY_END}`, + `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank)${ENTRY_END}`, ) expect(excludeFoldersEntry("vault_get_daily_note")).toBe(`${ENTRY_START}${ENTRY_END}`) }) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index 4bdc850f3..d3003d22a 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4235 + const CHARS_PER_TOOL_ALLOWANCE = 4268 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index de940564a..79d00b4eb 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -18,7 +18,7 @@ export const registerDailyNoteTools = ({ TOOL_NAMES.VAULT_GET_DAILY_NOTE, { title: "Get Daily Note", - description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder), then to "Daily Notes" and YYYY-MM-DD. + description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to "Daily Notes" and YYYY-MM-DD. Example: vault_get_daily_note({ date: "2026-05-13" }) Example: vault_get_daily_note({}) — returns today's daily note @@ -32,6 +32,7 @@ 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 +- "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 - "EACCES: …" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it 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." : "When exists is false, the note has not been created yet."}`, diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 2052efc5d..e58381ff3 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -589,7 +589,7 @@ For incoming links (what links TO a note), use vault_get_backlinks. Parameters: - path: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds). -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless its setting is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"). When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken (kind is always "note"). daily_note_forward_ref is true on broken links into the vault's daily notes folder, unless the daily notes folder setting is the vault root or blank — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk size of a note or file (null for broken links)${fileBytesClause}. Errors: - "path must end in …" — add the .md or .canvas extension @@ -648,7 +648,7 @@ Errors: ? "" : whenToolEnabledText( "vault_get_daily_note", - " (vault_get_daily_note's path starts with the daily notes folder, unless that folder is blank)", + " (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank)", ) registerTool( diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 673745ec0..9a09efe6b 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -1108,7 +1108,8 @@ Errors: - "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it - "concurrent write in progress" — another write to this note is in flight; retry - "note not found: …" — ${noteNotFoundVerifyRemedy}${trashMoveErrorEntries} -- "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry${trashConfigErrorEntry} +- "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry +${FILESYSTEM_ERROR_ENTRIES}${trashConfigErrorEntry} - "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry Returns: ${deleteNoteReturns} Notes how many empty folders were pruned when any were.`, @@ -1229,6 +1230,7 @@ Errors: - An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish. - A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move. - "move aborted: could not read …" / "move aborted: could not rewrite …" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry. +${FILESYSTEM_ERROR_ENTRIES} Obsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed. From 6ce1ab79b5f9d6eec25943b669bc8f4b08f8a7e2 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:10:29 -0400 Subject: [PATCH 20/26] fix(daily-notes): give delete and move their own filesystem-error entries and tighten the daily-note text The shared entries' .tmp clause cannot reach a delete or move client; delete's EACCES shares the cannot-delete remedy. The orphan hint no longer repeats the root/blank condition, and vault_get_daily_note says where the new-note folder comes from. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/tool-surface/default.json | 8 ++++---- .../__snapshots__/tool-surface/disabled-tools.json | 8 ++++---- .../__snapshots__/tool-surface/embedding-off.json | 8 ++++---- .../tool-surface/file-tools-off+embedding-off.json | 8 ++++---- .../__snapshots__/tool-surface/file-tools-off.json | 8 ++++---- .../tool-surface/memory-off+embedding-off.json | 8 ++++---- .../memory-off+file-tools-off+embedding-off.json | 8 ++++---- .../tool-surface/memory-off+file-tools-off.json | 8 ++++---- .../__snapshots__/tool-surface/memory-off.json | 8 ++++---- .../__snapshots__/tool-surface/obsidian-sync.json | 8 ++++---- .../tool-surface/readonly+embedding-off.json | 4 ++-- .../readonly+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off.json | 4 ++-- .../readonly+memory-off+embedding-off.json | 4 ++-- ...ly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly.json | 4 ++-- .../mcp-core/__tests__/tool-definitions.test.ts | 14 ++++++-------- .../__tests__/tool-surface-snapshot.test.ts | 2 +- src/vault-mcp/mcp-core/tools/daily-note-tools.ts | 4 ++-- src/vault-mcp/mcp-core/tools/search-tools.ts | 2 +- src/vault-mcp/mcp-core/tools/vault-crud-tools.ts | 7 ++++--- 23 files changed, 70 insertions(+), 71 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 8a2581dcb..a60f7c845 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 @@ -194,7 +194,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -334,7 +334,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 3fa3d9bce..bf61b9840 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 bc65ccdb7..80c729c3c 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 71adc8d28..fb112dcdf 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 @@ -197,7 +197,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -337,7 +337,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 29bdc7ebe..532e832f8 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 f2a734229..7f28321ae 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 e4a5a3a04..93c795791 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 @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -292,7 +292,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 4d6f7e72e..53085c8cc 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 @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 ea0b3e433..bcebeacaa 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 @@ -150,7 +150,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting (`trashOption` in `.obsidian/app.json`) decides the outcome:\n - \"Move to system trash\" (`system`, also what an absent setting means) moves the note to `.trash/`, since the server has no system trash. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none); notes Obsidian itself trashed are never touched.\n - \"Move to Obsidian trash\" (`local`) moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" (`none`) removes the note for good.\n- The caller can't choose or see the outcome in advance; the returned message says which happened.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- any other \"cannot move to trash …\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -290,7 +290,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 9123eef29..e9cc37b3e 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 @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good: this server syncs through Obsidian Sync, so it bypasses the vault's \"Deleted files\" setting; recover a deleted note from Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder the delete leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. Pruning runs after the delete or trash move and is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path), \"EACCES: …\" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in \".tmp\", the note exists but its name is too long to save; rename it at least 41 bytes shorter\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 094c55fe3..a9317f69c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 d134e335f..c4c4dfca0 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 dd794ea9b..fae20d10f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 70daaab12..f1c5be189 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 c990e4c57..3ab17234d 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 0828da880..29f54b779 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 171819c7b..6b110248b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 b8765c5d3..86ae41b1e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -71,7 +71,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 43b9ee4e1..723e46e7b 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -1114,13 +1114,11 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { '- "note not found: …" — verify path with vault_list_notes', ] const OTHER_DELETE_ERROR_ENTRY = - '- "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry' + '- "cannot delete …" (other than a protected path), "EACCES: …" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry' const DAILY_NOTES_CONFIG_ERROR_ENTRY = '- "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry' - const FILESYSTEM_ERROR_ENTRY_LINES = [ - '- "ENAMETOOLONG: …" — the path, or a name in it, is longer than the filesystem allows; check the path. If the quoted name ends in ".tmp", the note exists but its name is too long to save; rename it at least 41 bytes shorter', - "- \"EACCES: …\" or another filesystem error code — the note's file can't be read or written (permissions, a full or read-only disk); ask the vault's owner to fix it", - ] + const NAME_TOO_LONG_ERROR_ENTRY = + '- "ENAMETOOLONG: …" — the path, or a name in it, is longer than the filesystem allows; check the path' it("lists the trash-move and trash-setting errors when the server does not sync", () => { expect(deleteNoteErrors({})).toBe( @@ -1130,7 +1128,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { '- "cannot move to trash … — 100 collisions in .trash/" — .trash/ holds this name and 100 numbered copies ("Plan 1.md" … "Plan 100.md"); ask the vault owner to clear old copies, then retry', '- any other "cannot move to trash …" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry', OTHER_DELETE_ERROR_ENTRY, - ...FILESYSTEM_ERROR_ENTRY_LINES, + NAME_TOO_LONG_ERROR_ENTRY, "- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed, and guessing the setting could let the server's trash cleanup delete a note Obsidian keeps forever; ask the vault owner to repair it, then retry", DAILY_NOTES_CONFIG_ERROR_ENTRY, ].join("\n"), @@ -1143,7 +1141,7 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { "Errors:", ...PATH_AND_LOOKUP_ERROR_ENTRIES, OTHER_DELETE_ERROR_ENTRY, - ...FILESYSTEM_ERROR_ENTRY_LINES, + NAME_TOO_LONG_ERROR_ENTRY, DAILY_NOTES_CONFIG_ERROR_ENTRY, ].join("\n"), ) @@ -2718,7 +2716,7 @@ describe("DISABLED_TOOLS", () => { '. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank)${ENTRY_END}`, + `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder when one is excluded)${ENTRY_END}`, ) expect(excludeFoldersEntry("vault_get_daily_note")).toBe(`${ENTRY_START}${ENTRY_END}`) }) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index d3003d22a..e079cd49b 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4268 + const CHARS_PER_TOOL_ALLOWANCE = 4255 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index 79d00b4eb..40df0f833 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -18,12 +18,12 @@ export const registerDailyNoteTools = ({ TOOL_NAMES.VAULT_GET_DAILY_NOTE, { title: "Get Daily Note", - description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder, the vault root by default), then to "Daily Notes" and YYYY-MM-DD. + description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to "Daily Notes" and YYYY-MM-DD. 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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. ${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. ")}Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes). Parameters: - date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index e58381ff3..92663d790 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -648,7 +648,7 @@ Errors: ? "" : whenToolEnabledText( "vault_get_daily_note", - " (vault_get_daily_note's path starts with the daily notes folder, unless the daily notes folder setting is the vault root or blank)", + " (vault_get_daily_note's path starts with the daily notes folder when one is excluded)", ) registerTool( diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 9a09efe6b..d9470e623 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -1108,8 +1108,8 @@ Errors: - "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it - "concurrent write in progress" — another write to this note is in flight; retry - "note not found: …" — ${noteNotFoundVerifyRemedy}${trashMoveErrorEntries} -- "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry -${FILESYSTEM_ERROR_ENTRIES}${trashConfigErrorEntry} +- "cannot delete …" (other than a protected path), "EACCES: …" or another filesystem error code — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry +- "ENAMETOOLONG: …" — the path, or a name in it, is longer than the filesystem allows; check the path${trashConfigErrorEntry} - "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable, so the daily notes folder to protect is unknown; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry Returns: ${deleteNoteReturns} Notes how many empty folders were pruned when any were.`, @@ -1230,7 +1230,8 @@ Errors: - An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish. - A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move. - "move aborted: could not read …" / "move aborted: could not rewrite …" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry. -${FILESYSTEM_ERROR_ENTRIES} +- "ENAMETOOLONG: …" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path. +- "EACCES: …" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it. Obsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed. From ff15a318dedd6ba4041c78c9c813ed3d72de6852 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:21:52 -0400 Subject: [PATCH 21/26] fix(daily-notes): name the referents in the orphan hint and the unreadable-file clause Co-Authored-By: Claude Opus 5.5 --- .../__tests__/__snapshots__/tool-surface/default.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/disabled-tools.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/embedding-off.json | 4 ++-- .../tool-surface/file-tools-off+embedding-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/file-tools-off.json | 4 ++-- .../__snapshots__/tool-surface/memory-off+embedding-off.json | 4 ++-- .../tool-surface/memory-off+file-tools-off+embedding-off.json | 4 ++-- .../__snapshots__/tool-surface/memory-off+file-tools-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/obsidian-sync.json | 4 ++-- .../__snapshots__/tool-surface/readonly+embedding-off.json | 4 ++-- .../tool-surface/readonly+file-tools-off+embedding-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly+file-tools-off.json | 4 ++-- .../tool-surface/readonly+memory-off+embedding-off.json | 4 ++-- .../readonly+memory-off+file-tools-off+embedding-off.json | 4 ++-- .../tool-surface/readonly+memory-off+file-tools-off.json | 4 ++-- .../__snapshots__/tool-surface/readonly+memory-off.json | 4 ++-- .../__tests__/__snapshots__/tool-surface/readonly.json | 4 ++-- src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts | 2 +- .../mcp-core/__tests__/tool-surface-snapshot.test.ts | 2 +- src/vault-mcp/mcp-core/tools/daily-note-tools.ts | 2 +- src/vault-mcp/mcp-core/tools/search-tools.ts | 2 +- 22 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index a60f7c845..7d0f27225 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 @@ -271,7 +271,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -334,7 +334,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 bf61b9840..d94bfb02c 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 80c729c3c..221f961ab 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 fb112dcdf..ced6be96c 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 @@ -274,7 +274,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -337,7 +337,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 532e832f8..c0d349f0c 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 7f28321ae..71c823446 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 @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 93c795791..17f374155 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 @@ -229,7 +229,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -292,7 +292,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 53085c8cc..7b34b4f38 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 @@ -228,7 +228,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -291,7 +291,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 bcebeacaa..a46c697e4 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 @@ -227,7 +227,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -290,7 +290,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 e9cc37b3e..afc036b13 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 @@ -273,7 +273,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -336,7 +336,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily 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).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, create the note with vault_write_note using the returned path.", "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 a9317f69c..328575a75 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 c4c4dfca0..8ce8fe54c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 fae20d10f..f2f0c3b80 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 f1c5be189..9d620a3a8 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 3ab17234d..497b63d10 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 29f54b779..e9b5a05ce 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 6b110248b..7443bc7e4 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -72,7 +72,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "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 86ae41b1e..e58d8468a 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder when one is excluded). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", + "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan (self-links are ignored).\n\nExample: vault_find_orphans({})\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- With exclude_folders omitted, the defaults apply; the daily notes folder among them is re-read on each call: DAILY_NOTES_FOLDER, else .obsidian/daily-notes.json, else \"Daily Notes\" (also used when that file is unreadable). When that folder is the vault root or blank, no daily folder is excluded.\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one). Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by most recently modified. bytes is the on-disk file size.", "inputSchema": { "type": "object", "properties": { @@ -71,7 +71,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… 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\n- \"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\n- \"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\n- \"EACCES: …\" or another filesystem error code — the daily note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: 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). When exists is false, the note has not been created yet.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 723e46e7b..8fad9a9f1 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -2716,7 +2716,7 @@ describe("DISABLED_TOOLS", () => { '. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder when one is excluded)${ENTRY_END}`, + `${ENTRY_START} (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one)${ENTRY_END}`, ) expect(excludeFoldersEntry("vault_get_daily_note")).toBe(`${ENTRY_START}${ENTRY_END}`) }) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index e079cd49b..95592e688 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4255 + const CHARS_PER_TOOL_ALLOWANCE = 4256 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index 40df0f833..b26471b0f 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -18,7 +18,7 @@ export const registerDailyNoteTools = ({ TOOL_NAMES.VAULT_GET_DAILY_NOTE, { title: "Get Daily Note", - description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; an unreadable file counts as absent), then to "Daily Notes" and YYYY-MM-DD. + description: `Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json (a blank folder there means Obsidian's new-note folder from .obsidian/app.json, the vault root by default; either file counts as absent when unreadable), then to "Daily Notes" and YYYY-MM-DD. Example: vault_get_daily_note({ date: "2026-05-13" }) Example: vault_get_daily_note({}) — returns today's daily note diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index 92663d790..d78b2f610 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -648,7 +648,7 @@ Errors: ? "" : whenToolEnabledText( "vault_get_daily_note", - " (vault_get_daily_note's path starts with the daily notes folder when one is excluded)", + " (vault_get_daily_note's path starts with the daily notes folder whenever the defaults exclude one)", ) registerTool( From 5e08b88ca6eb3c64405bcb175106ef85f9174dc3 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:51:17 -0400 Subject: [PATCH 22/26] test(tool-surface): raise the per-tool allowance to 4,263 after rebasing onto main Co-Authored-By: Claude Opus 5.5 --- src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index 95592e688..8fc9a76e0 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4256 + const CHARS_PER_TOOL_ALLOWANCE = 4263 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", From 5d1c43546f0db671e45d6288ff288478dd3438eb Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:12:51 -0400 Subject: [PATCH 23/26] fix(move): list the could-not-write error with its shorter-name remedy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The move wraps every failure of its first write, including a new_path name too long for the temp file it stages, in "move aborted: could not write to …". That message had no Errors entry. Co-Authored-By: Claude Opus 5.5 --- .../mcp-core/__tests__/__snapshots__/tool-surface/default.json | 2 +- .../__tests__/__snapshots__/tool-surface/disabled-tools.json | 2 +- .../__tests__/__snapshots__/tool-surface/embedding-off.json | 2 +- .../tool-surface/file-tools-off+embedding-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/file-tools-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+embedding-off.json | 2 +- .../tool-surface/memory-off+file-tools-off+embedding-off.json | 2 +- .../__snapshots__/tool-surface/memory-off+file-tools-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/memory-off.json | 2 +- .../__tests__/__snapshots__/tool-surface/obsidian-sync.json | 2 +- src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts | 2 +- src/vault-mcp/mcp-core/tools/vault-crud-tools.ts | 1 + 12 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 7d0f27225..cd396e169 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 @@ -951,7 +951,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 d94bfb02c..620ab36aa 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 @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 221f961ab..be887a1f3 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 @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 ced6be96c..5d867a90d 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 @@ -913,7 +913,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 c0d349f0c..d2693038f 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 @@ -912,7 +912,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 71c823446..5dc3da02b 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 @@ -814,7 +814,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 17f374155..281891409 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 @@ -774,7 +774,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 7b34b4f38..2cb1464e8 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 @@ -773,7 +773,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 a46c697e4..bb6e2421b 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 @@ -813,7 +813,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "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 afc036b13..eb873550b 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 @@ -953,7 +953,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Moving a note any other way can silently break its backlinks.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders removes each parent folder of old_path that the move leaves with zero entries, up to but never including the vault root; a folder holding any file, even a hidden .DS_Store, is kept. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Without it, empty folders stay, matching Obsidian.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify it with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable, so the daily notes folder to protect is unknown; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them (notes cannot move from or into hidden paths, matching Obsidian).\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path.\n- \"EACCES: …\" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index 8fc9a76e0..7aa43c0f3 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4263 + const CHARS_PER_TOOL_ALLOWANCE = 4273 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index d9470e623..45fcd15a1 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -1230,6 +1230,7 @@ Errors: - An ordinary move that fails partway (rare: a permission or disk error) — no data is lost, and the error names what failed and the resulting state. The original is deleted last, after the destination and every backlink are written. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish. - A case-only rename that fails partway — the note is renamed in place first. If the rename failed: nothing was written. If a later link write failed: the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move. - "move aborted: could not read …" / "move aborted: could not rewrite …" — the note, or a note linking to it, could not be read or has a properties block the move cannot keep; nothing was written; fix that note (for a properties block, the error says how), then retry. +- "move aborted: could not write to …" — new_path could not be created; nothing was written. Saving stages a temp file 41 bytes longer than new_path's name, so a long name fails; retry with one at least 41 bytes shorter. If that fails, the folder or disk can't be written; ask the vault's owner to fix it. - "ENAMETOOLONG: …" — old_path or new_path, or a name in it, is longer than the filesystem allows; check the path. - "EACCES: …" or another filesystem error code — a note or folder can't be read or written (permissions, a full or read-only disk); nothing was written; ask the vault's owner to fix it. From 8c1a53a99b5f569dda179ad8f27f3dc3eb3e3e0b Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:16:24 -0400 Subject: [PATCH 24/26] docs(daily-notes): name each new-note location setting in readNewNoteFolder's comment Co-Authored-By: Claude Opus 5.5 --- src/vault-mcp/vault-operations/daily-notes.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index a3f96dacf..79eaf8b65 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -130,12 +130,14 @@ const readDailyNotesFile = async (vaultPath: string): Promise => { From 5e5ee1a0c1b31cae253acb657065b429dd83d55d Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:25:36 -0400 Subject: [PATCH 25/26] docs(daily-notes): say when the server reads app.json, and why the current-file option means the root Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- deploy/remote/README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7d209dc5..efc2ff60a 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ You can set one or both — a set value always wins over the config file. A valu - Changes in Obsidian apply on the next call when `.obsidian/daily-notes.json` is available in the server's vault. - Apply environment changes with the CLI's `restart` command, recreate the Compose container, or redeploy on your hosting platform. -- If you leave the folder empty in Obsidian's Daily notes options, the server follows Obsidian's **Default location for new notes** setting (Settings → Files and links), read from `.obsidian/app.json`. **Same folder as current file** counts as the vault root, since Obsidian creates a daily note with no current file to place it beside. When `app.json` isn't in the server's vault, as in remote mode with the default `SYNC_CONFIGS`, the server uses the vault root. +- If you leave the folder empty in Obsidian's Daily notes options, the server follows Obsidian's **Default location for new notes** setting (Settings → Files and links), read from `.obsidian/app.json`. **Same folder as current file** counts as the vault root, because Obsidian creates a daily note without reference to an open note and so has no folder to follow. When `app.json` isn't in the server's vault, as in remote mode with the default `SYNC_CONFIGS`, the server uses the vault root. - If your daily notes live in the vault root, or in the new-note folder because the folder setting is empty, delete and move don't protect them and orphan results can include them. Protecting or excluding that folder would also cover every other note in it. > **Note:** A few date format tokens are unsupported — ordinals (`Do`, `Mo`, `DDDo`, `wo`), `dd` (2-letter weekday), `d` (weekday number), `e`, `k`/`kk`, `w` (week number), `Q` (quarter), `Z`/`ZZ` (UTC offset), and the localized formats (`L`–`LLLL`, `LT`, `LTS`). The server can't reproduce the filenames Obsidian creates with these tokens, so it could never find the notes. If your format uses any of them, `vault_get_daily_note` returns a clear error — change the format in Obsidian or set `DAILY_NOTES_FORMAT` to a supported alternative. diff --git a/deploy/remote/README.md b/deploy/remote/README.md index aaf62bc94..2e433e087 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -551,8 +551,9 @@ that file when settings syncing is turned on in two places: Some community plugins keep API keys in their settings. Server tools never read `.obsidian/`, and the server itself opens only `daily-notes.json`, the -Tasks plugin's settings, and `app.json` when the `app` category is synced — -other synced settings just sit in the vault's `.obsidian/` folder. +Tasks plugin's settings, and `app.json` (for the new-note location, when the +daily notes folder is blank) — other synced settings just sit in the vault's +`.obsidian/` folder. If you turn on **Core plugin settings** syncing after the server is already running, `daily-notes.json` is picked up automatically once it From 444d720cef2e8670c0145f4f2ace0a6d430b7fb6 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:48:23 -0400 Subject: [PATCH 26/26] docs(daily-notes): say why the tolerant readers' fallbacks are safe Co-Authored-By: Claude Opus 5.5 --- src/vault-mcp/vault-operations/daily-notes.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vault-mcp/vault-operations/daily-notes.ts b/src/vault-mcp/vault-operations/daily-notes.ts index 79eaf8b65..3da840192 100644 --- a/src/vault-mcp/vault-operations/daily-notes.ts +++ b/src/vault-mcp/vault-operations/daily-notes.ts @@ -155,7 +155,9 @@ const readNewNoteFolder = async (vaultPath: string, logger: Logger): Promise