obsidian_shortest_path
Find the shortest path between two notes in an Obsidian vault, using forward, backward, or bidirectional link traversal with configurable depth limit.
Instructions
Find a shortest graph path between two notes.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| vault | No | Optional configured vault name. Defaults to the server default vault. | |
| from | Yes | Vault-relative path. Absolute paths and traversal are rejected. | |
| to | Yes | ||
| maxDepth | No | ||
| direction | No | both |
Implementation Reference
- src/graph.ts:119-149 (handler)The `shortestPath` function implements BFS over the vault link graph to find the shortest path between two notes. It is the core handler logic for the obsidian_shortest_path tool.
export function shortestPath( graph: VaultGraph, fromPath: string, toPath: string, options: { maxDepth: number; direction: "forward" | "backward" | "both" }, ): { path: string[]; edges: GraphEdge[] } | null { const from = resolveGraphPath(graph, fromPath); const to = resolveGraphPath(graph, toPath); if (!from || !to) return null; const queue: Array<{ path: string; route: string[]; edges: GraphEdge[] }> = [{ path: from.path, route: [from.path], edges: [] }]; const seen = new Set<string>([from.path]); while (queue.length > 0) { const current = queue.shift(); if (!current) continue; if (current.path === to.path) return { path: current.route, edges: current.edges }; if (current.route.length - 1 >= options.maxDepth) continue; const edges = graph.edges.filter((edge) => { if (edge.unresolved) return false; if (options.direction === "forward") return edge.source === current.path; if (options.direction === "backward") return edge.target === current.path; return edge.source === current.path || edge.target === current.path; }); for (const edge of edges) { const next = edge.source === current.path ? edge.target : edge.source; if (seen.has(next)) continue; seen.add(next); queue.push({ path: next, route: [...current.route, next], edges: [...current.edges, edge] }); } } return null; } - src/tools.ts:765-777 (schema)The schema definition for obsidian_shortest_path tool: accepts from, to, maxDepth, direction args. The schema is defined via Zod in the tool registration.
tool( "obsidian_shortest_path", "Find a shortest graph path between two notes.", { vault: vaultArg, from: pathArg, to: pathArg, maxDepth: z.number().int().min(1).max(10).optional().default(5), direction: z.enum(["forward", "backward", "both"]).optional().default("both"), }, async (args) => ({ result: shortestPath(buildGraph(await loadNotes(vaults, args.vault)), args.from, args.to, args) }), { readOnlyHint: true }, ); - src/tools.ts:766-777 (registration)The registration of the obsidian_shortest_path tool in registerObsidianTools, binding the schema to the handler that calls shortestPath().
"obsidian_shortest_path", "Find a shortest graph path between two notes.", { vault: vaultArg, from: pathArg, to: pathArg, maxDepth: z.number().int().min(1).max(10).optional().default(5), direction: z.enum(["forward", "backward", "both"]).optional().default("both"), }, async (args) => ({ result: shortestPath(buildGraph(await loadNotes(vaults, args.vault)), args.from, args.to, args) }), { readOnlyHint: true }, ); - src/graph.ts:151-166 (helper)`resolveGraphPath` is a helper used by shortestPath to resolve a note path/name to a GraphNode, trying multiple candidate paths and stems.
export function resolveGraphPath(graph: VaultGraph, input: string): GraphNode | null { const clean = normalizeNoteTarget(input); const candidates = [ clean, clean.endsWith(".md") ? clean : `${clean}.md`, path.posix.basename(clean), `${path.posix.basename(clean)}.md`, ]; for (const candidate of candidates) { const direct = graph.byPath.get(candidate); if (direct) return direct; } const stem = path.posix.basename(clean).replace(/\.md$/i, "").toLowerCase(); const found = [...graph.byPath.values()].find((node) => noteStem(node.path).toLowerCase() === stem); return found ?? null; } - src/graph.ts:168-188 (helper)`createResolver` is a helper used by buildGraph to resolve link targets to file paths, which feeds into the graph edges used by shortestPath.
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; }; }