get_graph_neighbors
Retrieve notes within specified link-hops of a starting note in Obsidian vaults, enabling graph traversal and link analysis.
Instructions
Get notes within N link-hops of a given note
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | The starting note path (relative to vault root) | |
| depth | No | Maximum number of link-hops (1-5) | |
| direction | No | Direction to traverse: inbound (backlinks), outbound (outlinks), or both | both |
Implementation Reference
- src/tools/links.ts:482-639 (handler)The 'get_graph_neighbors' tool is registered and implemented in this block, using BFS to traverse the link graph and return neighbor notes within a specified depth and direction.
server.registerTool( "get_graph_neighbors", { description: "Get notes within N link-hops of a given note", inputSchema: { path: z.string().min(1).describe("The starting note path (relative to vault root)"), depth: z .number() .int() .min(1) .max(5) .optional() .default(1) .describe("Maximum number of link-hops (1-5)"), direction: z .enum(["both", "inbound", "outbound"]) .optional() .default("both") .describe("Direction to traverse: inbound (backlinks), outbound (outlinks), or both"), }, }, async ({ path: startPath, depth, direction }) => { try { const graph = await buildLinkGraph(vaultPath); // Resolve the start path const startNormalized = startPath.replace(/\.md$/i, "").toLowerCase(); let resolvedStart: string | null = null; for (const notePath of graph.allNotes) { const noteNormalized = notePath.replace(/\.md$/i, "").toLowerCase(); if (noteNormalized === startNormalized) { resolvedStart = notePath; break; } } if (!resolvedStart) { // Try basename matching const startBasename = startNormalized.split("/").pop() ?? startNormalized; for (const notePath of graph.allNotes) { const noteBasename = notePath .replace(/\.md$/i, "") .split("/") .pop() ?.toLowerCase(); if (noteBasename === startBasename) { resolvedStart = notePath; break; } } } if (!resolvedStart) { return errorResult(`No note found matching path: ${startPath}`); } // BFS traversal const visited = new Map<string, GraphNeighbor>(); const queue: { path: string; currentDepth: number }[] = [ { path: resolvedStart, currentDepth: 0 }, ]; visited.set(resolvedStart, { path: resolvedStart, depth: 0, direction: "both", }); while (queue.length > 0) { const { path: currentPath, currentDepth } = queue.shift()!; if (currentDepth >= depth) continue; const neighbors: { path: string; dir: "inbound" | "outbound" }[] = []; if (direction === "outbound" || direction === "both") { const outs = graph.outlinks.get(currentPath); if (outs) { for (const target of outs) { neighbors.push({ path: target, dir: "outbound" }); } } } if (direction === "inbound" || direction === "both") { const ins = graph.backlinks.get(currentPath); if (ins) { for (const source of ins) { neighbors.push({ path: source, dir: "inbound" }); } } } for (const neighbor of neighbors) { if (!visited.has(neighbor.path)) { const neighborInfo: GraphNeighbor = { path: neighbor.path, depth: currentDepth + 1, direction: neighbor.dir, }; visited.set(neighbor.path, neighborInfo); queue.push({ path: neighbor.path, currentDepth: currentDepth + 1 }); } } } // Remove the start node from results visited.delete(resolvedStart); if (visited.size === 0) { return { content: [ { type: "text" as const, text: `No neighbors found for: ${resolvedStart} (depth: ${depth}, direction: ${direction})`, }, ], }; } // Group by depth level for tree-like output const byDepth = new Map<number, GraphNeighbor[]>(); for (const neighbor of visited.values()) { if (!byDepth.has(neighbor.depth)) { byDepth.set(neighbor.depth, []); } byDepth.get(neighbor.depth)!.push(neighbor); } const lines: string[] = [ `Graph neighbors of: ${resolvedStart}`, `Direction: ${direction} | Max depth: ${depth} | Found: ${visited.size} note(s)\n`, resolvedStart, ]; const sortedDepths = [...byDepth.keys()].sort((a, b) => a - b); for (const d of sortedDepths) { const neighbors = byDepth.get(d)!; neighbors.sort((a, b) => a.path.localeCompare(b.path)); for (const neighbor of neighbors) { const indent = " ".repeat(d); const arrow = neighbor.direction === "inbound" ? "←" : neighbor.direction === "outbound" ? "→" : "↔"; lines.push(`${indent}${arrow} ${neighbor.path} (depth ${d})`); } } return { content: [{ type: "text" as const, text: lines.join("\n") }] }; } catch (err) { console.error("get_graph_neighbors error:", err); return errorResult(`Error getting graph neighbors: ${err instanceof Error ? err.message : String(err)}`); } }, );