obsidian_query_notes
Query your Obsidian notes using tags, folders, frontmatter, or links. Filter, sort, and limit results to find specific notes quickly.
Instructions
Structured Dataview-like query over local notes: tags, folders, linksTo, frontmatter equality/regex, sort, limit.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| vault | No | Optional configured vault name. Defaults to the server default vault. | |
| tags | No | ||
| folders | No | ||
| linksTo | No | ||
| frontmatter | No | ||
| sortBy | No | path | |
| sortDirection | No | asc | |
| limit | No | ||
| offset | No |
Implementation Reference
- src/tools.ts:342-361 (registration)Registration of the 'obsidian_query_notes' tool with Zod schema (tags, folders, linksTo, frontmatter, sortBy, sortDirection, limit, offset) and handler that calls queryNotes from search.ts.
tool( "obsidian_query_notes", "Structured Dataview-like query over local notes: tags, folders, linksTo, frontmatter equality/regex, sort, limit.", { vault: vaultArg, tags: z.array(z.string()).optional(), folders: z.array(z.string()).optional(), linksTo: z.string().optional(), frontmatter: z.record(z.unknown()).optional(), sortBy: z.enum(["path", "title", "mtime", "size"]).optional().default("path"), sortDirection: z.enum(["asc", "desc"]).optional().default("asc"), limit: z.number().int().min(1).max(500).optional().default(100), offset: z.number().int().min(0).optional().default(0), }, async (args) => { const all = queryNotes(await loadNotes(vaults, args.vault), { ...args, limit: args.offset + args.limit }); return { total: all.length, offset: args.offset, notes: all.slice(args.offset, args.offset + args.limit) }; }, { readOnlyHint: true }, ); - src/search.ts:108-150 (handler)Core implementation of the queryNotes function that filters notes by tags, folders, linksTo, and frontmatter, then sorts and paginates results.
export function queryNotes( notes: NoteRecord[], options: { tags?: string[]; folders?: string[]; linksTo?: string; frontmatter?: Record<string, unknown>; sortBy?: "path" | "title" | "mtime" | "size"; sortDirection?: "asc" | "desc"; limit: number; }, ): Array<Omit<NoteRecord, "content"> & { links: string[] }> { const tags = (options.tags ?? []).map((t) => t.replace(/^#/, "").toLowerCase()); const folders = (options.folders ?? []).map((f) => f.replace(/^\/+/, "").toLowerCase()); const linksTo = options.linksTo?.replace(/\.md$/i, "").toLowerCase(); const rows = notes .filter((note) => { if (tags.length > 0 && !tags.every((tag) => note.tags.some((t) => t.toLowerCase() === tag || t.toLowerCase().startsWith(`${tag}/`)))) return false; if (folders.length > 0 && !folders.some((folder) => note.path.toLowerCase().startsWith(folder))) return false; if (options.frontmatter) { for (const [key, expected] of Object.entries(options.frontmatter)) { if (!frontmatterMatches(note.frontmatter[key], expected)) return false; } } if (linksTo) { const links = extractWikiLinks(note.content).map((link) => link.target.replace(/\.md$/i, "").toLowerCase()); if (!links.includes(linksTo) && !links.includes(noteStem(`${linksTo}.md`).toLowerCase())) return false; } return true; }) .map((note) => { const { content: _content, ...rest } = note; return { ...rest, links: extractWikiLinks(note.content).map((link) => link.target) }; }); const sortBy = options.sortBy ?? "path"; rows.sort((a, b) => { const av = a[sortBy]; const bv = b[sortBy]; const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv)); return options.sortDirection === "desc" ? -cmp : cmp; }); return rows.slice(0, options.limit); } - src/search.ts:234-241 (helper)The frontmatterMatches helper function used by queryNotes to compare frontmatter values (supports arrays, regex patterns with /pattern/ syntax, and case-insensitive string comparison).
function frontmatterMatches(actual: unknown, expected: unknown): boolean { if (Array.isArray(actual)) return actual.some((value) => frontmatterMatches(value, expected)); if (Array.isArray(expected)) return expected.some((value) => frontmatterMatches(actual, value)); if (typeof expected === "string" && expected.startsWith("/") && expected.endsWith("/")) { return new RegExp(expected.slice(1, -1), "i").test(String(actual ?? "")); } return String(actual ?? "").toLowerCase() === String(expected ?? "").toLowerCase(); } - src/tools.ts:38-60 (helper)The local 'tool' helper function used to register MCP tools with annotations, wrapping the server.tool call.
export function registerObsidianTools(server: McpServer, vaults: VaultManager, config: ObsidianMcpConfig): void { vaults.onInvalidate = invalidateNotesCache; const pretty = config.pretty; const tool = <S extends ToolShape>( name: string, description: string, schema: S, handler: (args: z.objectOutputType<S, z.ZodTypeAny>) => Promise<unknown> | unknown, annotations: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean } = {}, ) => { (server.tool as any)( name, description, schema, { readOnlyHint: annotations.readOnlyHint ?? false, destructiveHint: annotations.destructiveHint ?? false, idempotentHint: annotations.idempotentHint ?? false, openWorldHint: false, }, async (args: unknown) => jsonResult(await handler(args as z.objectOutputType<S, z.ZodTypeAny>), pretty), ); }; - src/search.ts:34-56 (helper)The loadNotes function that reads all markdown files from the vault, used by the queryNotes handler to get data.
export async function loadNotes(vaults: VaultManager, vault?: string | null): Promise<NoteRecord[]> { const vaultName = vaults.getVault(vault).name; const cached = notesCache.get(vaultName); if (cached && Date.now() - cached.timestamp < NOTES_CACHE_TTL) return cached.notes; const files = await vaults.markdownFiles(vault); const records: NoteRecord[] = []; for (const file of files) { const read = await vaults.readText(file, vault); const parsed = parseFrontmatter(read.text); records.push({ path: read.path, title: titleFor(read.path, read.text, parsed.data), content: read.text, frontmatter: parsed.data, tags: extractAllTags(read.text), headings: extractHeadings(read.text), mtime: read.stat.mtime.toISOString(), size: read.stat.size, }); } notesCache.set(vaultName, { notes: records, timestamp: Date.now() }); return records; }