Get Vault Stats
get_vault_statsRetrieves a vault health snapshot including note count, total bytes and words, unique tags, untagged notes, and most-recently-modified note to quickly assess vault condition.
Instructions
Return a quick health snapshot of the vault: note count, total bytes, total words, unique tag count, untagged-note count, and the most-recently-modified note. Useful for dashboards and 'is this vault healthy?' checks. Reads through the mtime cache so repeat calls are cheap.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| folder | No | Restrict stats to this folder (relative to vault root). Omit for whole-vault stats. |
Implementation Reference
- src/tools/read.ts:530-620 (handler)The handler function for the 'get_vault_stats' tool. Lists notes in the vault (optionally filtered by folder), reads their content via mtime cache, computes total bytes, total words, unique tag count, untagged note count, and most-recently-modified note via fs.stat, then returns a formatted text snapshot.
server.registerTool( "get_vault_stats", { title: "Get Vault Stats", description: "Return a quick health snapshot of the vault: note count, total bytes, total words, unique tag count, untagged-note count, and the most-recently-modified note. Useful for dashboards and 'is this vault healthy?' checks. Reads through the mtime cache so repeat calls are cheap.", annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, inputSchema: { folder: z .string() .optional() .describe("Restrict stats to this folder (relative to vault root). Omit for whole-vault stats."), }, }, async ({ folder }) => { try { const notes = await listNotes(vaultPath, folder); if (notes.length === 0) { return { content: [{ type: "text" as const, text: folder ? `No notes in "${folder}"` : "Vault is empty." }] }; } const { contents } = await readAllCached(vaultPath, notes, (note, err) => { log.warn("get_vault_stats: note read failed", { note, err }); }); let totalBytes = 0; let totalWords = 0; let untagged = 0; const tagSet = new Set<string>(); for (const notePath of notes) { const content = contents.get(notePath); if (content === undefined) continue; totalBytes += Buffer.byteLength(content, "utf-8"); // Word count: parse frontmatter out so YAML keys don't inflate // the count, then split body on whitespace. const { content: body } = parseFrontmatter(content); const matches = body.match(/\S+/g); totalWords += matches ? matches.length : 0; const tags = extractTags(content); if (tags.length === 0) untagged++; for (const t of tags) tagSet.add(t.toLowerCase()); } // Most recent note via fs.stat — keep this independent of the cache // so it's accurate even if the cache hasn't been touched yet. let mostRecent: { path: string; mtimeMs: number } | null = null; const stats = await mapConcurrent<string, { path: string; mtimeMs: number } | undefined>( notes, 16, async (notePath) => { try { const st = await getNoteStats(vaultPath, notePath); if (!st.modified) return undefined; return { path: notePath, mtimeMs: st.modified.getTime() }; } catch { return undefined; } }, ); for (const s of stats) { if (!s) continue; if (!mostRecent || s.mtimeMs > mostRecent.mtimeMs) mostRecent = s; } const avgBytes = Math.round(totalBytes / notes.length); const avgWords = Math.round(totalWords / notes.length); const untaggedPct = ((untagged / notes.length) * 100).toFixed(1); const lines = [ `Vault stats${folder ? ` (folder: ${folder})` : ""}`, "", ` Notes: ${notes.length}`, ` Total bytes: ${totalBytes.toLocaleString()}`, ` Total words: ${totalWords.toLocaleString()}`, ` Avg bytes/note: ${avgBytes.toLocaleString()}`, ` Avg words/note: ${avgWords.toLocaleString()}`, ` Unique tags: ${tagSet.size}`, ` Untagged notes: ${untagged} (${untaggedPct}%)`, mostRecent ? ` Most recent: ${mostRecent.path} (${new Date(mostRecent.mtimeMs).toISOString()})` : ` Most recent: (none)`, ]; return { content: [{ type: "text" as const, text: lines.join("\n") }] }; } catch (err) { log.error("get_vault_stats failed", { tool: "get_vault_stats", err: err as Error }); return errorResult(`Error gathering vault stats: ${sanitizeError(err)}`); } }, ); - src/tools/read.ts:532-546 (schema)Input schema for 'get_vault_stats'. Accepts an optional 'folder' string to restrict stats to a subfolder of the vault. Uses Zod validation with a description.
{ title: "Get Vault Stats", description: "Return a quick health snapshot of the vault: note count, total bytes, total words, unique tag count, untagged-note count, and the most-recently-modified note. Useful for dashboards and 'is this vault healthy?' checks. Reads through the mtime cache so repeat calls are cheap.", annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, inputSchema: { folder: z .string() .optional() .describe("Restrict stats to this folder (relative to vault root). Omit for whole-vault stats."), }, - src/tools/read.ts:530-620 (registration)Registration of 'get_vault_stats' tool with the MCP server via server.registerTool() inside the registerReadTools function (defined at line 15).
server.registerTool( "get_vault_stats", { title: "Get Vault Stats", description: "Return a quick health snapshot of the vault: note count, total bytes, total words, unique tag count, untagged-note count, and the most-recently-modified note. Useful for dashboards and 'is this vault healthy?' checks. Reads through the mtime cache so repeat calls are cheap.", annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, inputSchema: { folder: z .string() .optional() .describe("Restrict stats to this folder (relative to vault root). Omit for whole-vault stats."), }, }, async ({ folder }) => { try { const notes = await listNotes(vaultPath, folder); if (notes.length === 0) { return { content: [{ type: "text" as const, text: folder ? `No notes in "${folder}"` : "Vault is empty." }] }; } const { contents } = await readAllCached(vaultPath, notes, (note, err) => { log.warn("get_vault_stats: note read failed", { note, err }); }); let totalBytes = 0; let totalWords = 0; let untagged = 0; const tagSet = new Set<string>(); for (const notePath of notes) { const content = contents.get(notePath); if (content === undefined) continue; totalBytes += Buffer.byteLength(content, "utf-8"); // Word count: parse frontmatter out so YAML keys don't inflate // the count, then split body on whitespace. const { content: body } = parseFrontmatter(content); const matches = body.match(/\S+/g); totalWords += matches ? matches.length : 0; const tags = extractTags(content); if (tags.length === 0) untagged++; for (const t of tags) tagSet.add(t.toLowerCase()); } // Most recent note via fs.stat — keep this independent of the cache // so it's accurate even if the cache hasn't been touched yet. let mostRecent: { path: string; mtimeMs: number } | null = null; const stats = await mapConcurrent<string, { path: string; mtimeMs: number } | undefined>( notes, 16, async (notePath) => { try { const st = await getNoteStats(vaultPath, notePath); if (!st.modified) return undefined; return { path: notePath, mtimeMs: st.modified.getTime() }; } catch { return undefined; } }, ); for (const s of stats) { if (!s) continue; if (!mostRecent || s.mtimeMs > mostRecent.mtimeMs) mostRecent = s; } const avgBytes = Math.round(totalBytes / notes.length); const avgWords = Math.round(totalWords / notes.length); const untaggedPct = ((untagged / notes.length) * 100).toFixed(1); const lines = [ `Vault stats${folder ? ` (folder: ${folder})` : ""}`, "", ` Notes: ${notes.length}`, ` Total bytes: ${totalBytes.toLocaleString()}`, ` Total words: ${totalWords.toLocaleString()}`, ` Avg bytes/note: ${avgBytes.toLocaleString()}`, ` Avg words/note: ${avgWords.toLocaleString()}`, ` Unique tags: ${tagSet.size}`, ` Untagged notes: ${untagged} (${untaggedPct}%)`, mostRecent ? ` Most recent: ${mostRecent.path} (${new Date(mostRecent.mtimeMs).toISOString()})` : ` Most recent: (none)`, ]; return { content: [{ type: "text" as const, text: lines.join("\n") }] }; } catch (err) { log.error("get_vault_stats failed", { tool: "get_vault_stats", err: err as Error }); return errorResult(`Error gathering vault stats: ${sanitizeError(err)}`); } }, ); - src/lib/index-cache.ts:242-308 (helper)readAllCached helper: reads note contents with mtime-based caching. Used by the handler to efficiently retrieve note bodies for word counting and tag extraction.
export async function readAllCached( vaultPath: string, relPaths: readonly string[], onError?: (relPath: string, err: Error) => void, ): Promise<ReadAllResult> { const state = stateFor(vaultPath); await loadFromDisk(vaultPath, state); const cache = state.entries; const seen = new Set<string>(); const contents = new Map<string, string>(); let cacheHits = 0; let cacheMisses = 0; await mapConcurrent(relPaths, READ_CONCURRENCY, async (relPath) => { seen.add(relPath); let fullPath: string; try { fullPath = await resolveVaultPathSafe(vaultPath, relPath); } catch (err) { onError?.(relPath, err as Error); return undefined; } let mtimeMs: number; try { const stat = await fs.stat(fullPath); mtimeMs = stat.mtimeMs; } catch (err) { // ENOENT during stat means the file disappeared between listing and // reading — drop the cache entry and skip. if (cache.delete(relPath)) state.dirty = true; onError?.(relPath, err as Error); return undefined; } const cached = cache.get(relPath); if (cached && cached.mtimeMs === mtimeMs && cached.fullPath === fullPath) { contents.set(relPath, cached.content); cacheHits++; return undefined; } let content: string; try { content = await fs.readFile(fullPath, "utf-8"); } catch (err) { onError?.(relPath, err as Error); if (cache.delete(relPath)) state.dirty = true; return undefined; } cache.set(relPath, { fullPath, relPath, content, mtimeMs }); state.dirty = true; contents.set(relPath, content); cacheMisses++; return undefined; }); // Prune entries that weren't asked for this round. This stops the cache // from holding stale paths after a vault reorg or folder filter change. for (const key of cache.keys()) { if (!seen.has(key)) { cache.delete(key); state.dirty = true; } } if (state.dirty) scheduleFlush(vaultPath, state); return { contents, cacheHits, cacheMisses }; } - src/lib/vault.ts:764-776 (helper)getNoteStats helper: returns file stats (size, created, modified) via fs.stat. Used by the handler to find the most recently modified note.
export async function getNoteStats( vaultPath: string, relativePath: string, ): Promise<{ size: number; created: Date | null; modified: Date | null }> { const fullPath = await resolveVaultPathSafe(vaultPath, relativePath); const stats = await fs.stat(fullPath); return { size: stats.size, created: stats.birthtime ?? null, modified: stats.mtime ?? null, }; }