obsidian_find_broken_links
Find wiki and Markdown links that point to nonexistent notes in your Obsidian vault.
Instructions
List wiki/Markdown links that do not resolve to a note in the vault.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| vault | No | Optional configured vault name. Defaults to the server default vault. | |
| limit | No | ||
| offset | No |
Implementation Reference
- src/tools.ts:800-809 (registration)Registration of the 'obsidian_find_broken_links' tool. Includes schema (vault, limit, offset) and inline handler.
tool( "obsidian_find_broken_links", "List wiki/Markdown links that do not resolve to a note in the vault.", { vault: vaultArg, limit: z.number().int().min(1).max(2000).optional().default(500), offset: z.number().int().min(0).optional().default(0) }, async (args) => { const broken = buildGraph(await loadNotes(vaults, args.vault)).edges.filter((edge) => edge.unresolved); return { total: broken.length, offset: args.offset, links: broken.slice(args.offset, args.offset + args.limit) }; }, { readOnlyHint: true }, ); - src/tools.ts:804-807 (handler)Handler function: loads all notes, builds the link graph via buildGraph(), filters edges with unresolved=true, and returns paginated results.
async (args) => { const broken = buildGraph(await loadNotes(vaults, args.vault)).edges.filter((edge) => edge.unresolved); return { total: broken.length, offset: args.offset, links: broken.slice(args.offset, args.offset + args.limit) }; }, - src/graph.ts:28-65 (helper)buildGraph() iterates over all notes, extracts wiki and markdown links, and creates GraphEdge entries. The 'unresolved' flag is set when the resolver cannot find a matching note, which is the core logic that powers broken-link detection.
export function buildGraph(notes: NoteRecord[]): VaultGraph { const resolver = createResolver(notes); const edges: GraphEdge[] = []; for (const note of notes) { for (const link of extractWikiLinks(note.content)) { const target = resolver(link.target); edges.push({ source: note.path, target: target ?? link.target, rawTarget: link.target, kind: "wiki", unresolved: !target, line: link.line, }); } for (const link of extractMarkdownLinks(note.content)) { const target = resolver(link.target); edges.push({ source: note.path, target: target ?? link.target, rawTarget: link.target, kind: "markdown", unresolved: !target, line: link.line, }); } } const outCounts = countBy(edges.filter((e) => !e.unresolved), (edge) => edge.source); const inCounts = countBy(edges.filter((e) => !e.unresolved), (edge) => edge.target); const nodes = notes.map((note) => ({ path: note.path, title: note.title, tags: note.tags, outDegree: outCounts.get(note.path) ?? 0, inDegree: inCounts.get(note.path) ?? 0, })); return { nodes, edges, byPath: new Map(nodes.map((node) => [node.path, node])) }; } - src/graph.ts:168-188 (helper)createResolver() builds lookup maps (by path, by stem) from all note records. Returns a resolver function that returns the resolved note path or null if the link target cannot be matched — this null return is what sets unresolved=true on edges.
export function createResolver(notes: NoteRecord[]): (target: string) => string | null { const byPath = new Map<string, string>(); const byStem = new Map<string, string[]>(); for (const note of notes) { byPath.set(note.path.toLowerCase(), note.path); byPath.set(note.path.replace(/\.md$/i, "").toLowerCase(), note.path); byPath.set(path.posix.basename(note.path).toLowerCase(), note.path); const stem = noteStem(note.path).toLowerCase(); const list = byStem.get(stem) ?? []; list.push(note.path); byStem.set(stem, list); } return (target: string) => { const clean = normalizeNoteTarget(target).toLowerCase(); const direct = byPath.get(clean) ?? byPath.get(`${clean}.md`); if (direct) return direct; const stem = path.posix.basename(clean).replace(/\.md$/i, ""); const matches = byStem.get(stem); return matches?.length === 1 ? matches[0] : null; }; } - src/tools.ts:803-803 (schema)Zod schema for obsidian_find_broken_links: vault (optional string), limit (1-2000, default 500), offset (0+, default 0).
{ vault: vaultArg, limit: z.number().int().min(1).max(2000).optional().default(500), offset: z.number().int().min(0).optional().default(0) },