Skip to main content
Glama

get_graph_neighbors

Retrieve notes within specified link-hops of a starting note in Obsidian vaults to analyze connections and discover related content.

Instructions

Get notes within N link-hops of a given note

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYesThe starting note path (relative to vault root)
depthNoMaximum number of link-hops (1-5)
directionNoDirection to traverse: inbound (backlinks), outbound (outlinks), or bothboth

Implementation Reference

  • The handler function that executes the graph neighbor traversal logic using BFS.
      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)}`);
        }
      },
    );
  • Tool registration and input schema definition for get_graph_neighbors.
    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"),
        },
      },
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It states what the tool does but doesn't describe the return format (e.g., list of note paths, metadata), pagination, error conditions, or performance characteristics. For a read operation with no annotation coverage, this leaves significant gaps in understanding how the tool behaves.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose without unnecessary words. Every element ('Get notes', 'within N link-hops', 'of a given note') contributes directly to understanding the tool's function, with zero waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (3 parameters, no output schema, no annotations), the description is adequate but incomplete. It covers the purpose concisely but lacks details on return values, error handling, or usage context. Without annotations or output schema, the agent must rely on the description alone, which falls short of providing full operational context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with clear descriptions for all parameters (path, depth, direction). The description adds marginal value by hinting at the 'N link-hops' concept, which aligns with the depth parameter, but doesn't provide additional semantics beyond what the schema already documents. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Get notes') with a precise scope ('within N link-hops of a given note'), distinguishing it from siblings like get_backlinks (inbound only), get_outlinks (outbound only), and search_notes (general search). It uses a verb+resource+constraint structure that is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for finding connected notes via links, but doesn't explicitly state when to use this tool versus alternatives like get_backlinks, get_outlinks, or search_notes. No guidance on prerequisites or exclusions is provided, leaving the agent to infer context from the tool name and parameters.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rps321321/obsidian-mcp-pro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server