Skip to main content
Glama

Read Canvas

read_canvas
Read-onlyIdempotent

Read an Obsidian canvas file and generate a structured summary of all nodes and edges with positions, sizes, and content previews to inspect canvas structure before adding new elements.

Instructions

Read an Obsidian canvas file (.canvas, JSON format) and return a human-readable summary of its structure: every node with id, type, position, size, and content preview, plus every edge with source/target node ids and optional label. Use to inspect or navigate a canvas before calling add_canvas_node or add_canvas_edge.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYesRelative path from vault root to the .canvas file (e.g., 'boards/roadmap.canvas')

Implementation Reference

  • The read_canvas tool handler. Registered via server.registerTool('read_canvas', ...). Reads a .canvas file using readCanvasFile helper, formats a human-readable summary of nodes (id, type, position, size, content preview) and edges (source/target with optional label and side info), and returns it as text content.
    server.registerTool(
      "read_canvas",
      {
        title: "Read Canvas",
        description:
          "Read an Obsidian canvas file (.canvas, JSON format) and return a human-readable summary of its structure: every node with id, type, position, size, and content preview, plus every edge with source/target node ids and optional label. Use to inspect or navigate a canvas before calling add_canvas_node or add_canvas_edge.",
        annotations: {
          readOnlyHint: true,
          idempotentHint: true,
          openWorldHint: false,
        },
        inputSchema: {
          path: z
            .string()
            .min(1)
            .describe("Relative path from vault root to the .canvas file (e.g., 'boards/roadmap.canvas')"),
        },
      },
      async ({ path: canvasPath }) => {
        try {
          const data = await readCanvasFile(vaultPath, canvasPath);
          const lines: string[] = [];
    
          lines.push(`Canvas: ${canvasPath}`);
          lines.push(`Nodes: ${data.nodes.length} | Edges: ${data.edges.length}`);
          lines.push("");
    
          if (data.nodes.length > 0) {
            lines.push("--- Nodes ---");
            for (const node of data.nodes) {
              const pos = `(${node.x}, ${node.y})`;
              const size = `${node.width}x${node.height}`;
              let preview = "";
    
              if (node.type === "text" && node.text) {
                preview = node.text.length > 100
                  ? node.text.slice(0, 100) + "..."
                  : node.text;
              } else if (node.type === "file" && node.file) {
                preview = node.file;
              } else if (node.type === "link" && node.url) {
                preview = node.url;
              } else if (node.type === "group" && node.label) {
                preview = `Group: ${node.label}`;
              }
    
              lines.push(`  [${node.id}] type=${node.type} pos=${pos} size=${size}`);
              if (preview) {
                lines.push(`    content: ${preview}`);
              }
              if (node.color) {
                lines.push(`    color: ${node.color}`);
              }
            }
            lines.push("");
          }
    
          if (data.edges.length > 0) {
            lines.push("--- Edges ---");
            for (const edge of data.edges) {
              const label = edge.label ? ` [${edge.label}]` : "";
              const sides = [
                edge.fromSide ? `from-side=${edge.fromSide}` : "",
                edge.toSide ? `to-side=${edge.toSide}` : "",
              ].filter(Boolean).join(" ");
              const sideInfo = sides ? ` (${sides})` : "";
              lines.push(`  ${edge.fromNode} -> ${edge.toNode}${label}${sideInfo}`);
            }
          }
    
          return { content: [{ type: "text" as const, text: lines.join("\n") }] };
        } catch (err) {
          log.error("read_canvas failed", { tool: "read_canvas", err: err as Error });
          return errorResult(`Error reading canvas: ${sanitizeError(err)}`);
        }
      },
    );
  • Registration of the 'read_canvas' tool on the MCP server within registerCanvasTools().
    server.registerTool(
      "read_canvas",
      {
  • Input schema for read_canvas: requires a 'path' string (relative .canvas file path from vault root), validated via zod.
    inputSchema: {
      path: z
        .string()
        .min(1)
        .describe("Relative path from vault root to the .canvas file (e.g., 'boards/roadmap.canvas')"),
    },
  • The readCanvasFile helper function. Resolves the vault-relative path to an absolute path, reads the file as UTF-8, parses JSON, and returns a CanvasData object with nodes and edges arrays.
    export async function readCanvasFile(
      vaultPath: string,
      relativePath: string,
    ): Promise<CanvasData> {
      const fullPath = await resolveVaultPathSafe(vaultPath, relativePath);
      const content = await fs.readFile(fullPath, "utf-8");
      let parsed: unknown;
      try {
        parsed = JSON.parse(content);
      } catch {
        throw new Error(`Invalid canvas file (malformed JSON): ${relativePath}`);
      }
      const data = parsed as Record<string, unknown>;
      if (!Array.isArray(data.nodes)) {
        return { nodes: [], edges: [] };
      }
      return {
        nodes: data.nodes as CanvasData["nodes"],
        edges: Array.isArray(data.edges) ? data.edges as CanvasData["edges"] : [],
      };
    }
  • Type definitions for CanvasNode, CanvasEdge, and CanvasData interfaces used by the read_canvas tool.
    export interface CanvasNode {
      id: string;
      type: "text" | "file" | "link" | "group";
      x: number;
      y: number;
      width: number;
      height: number;
      text?: string;
      file?: string;
      url?: string;
      label?: string;
      color?: string;
    }
    
    export interface CanvasEdge {
      id: string;
      fromNode: string;
      toNode: string;
      fromSide?: "top" | "right" | "bottom" | "left";
      toSide?: "top" | "right" | "bottom" | "left";
      fromEnd?: "none" | "arrow";
      toEnd?: "none" | "arrow";
      label?: string;
      color?: string;
    }
    
    export interface CanvasData {
      nodes: CanvasNode[];
      edges: CanvasEdge[];
Behavior4/5

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

Annotations already declare readOnlyHint=true and idempotentHint=true, so the description does not need to re-state safety. It adds value by describing the output structure (nodes with id, type, position, size, content preview; edges with source/target ids and optional label), giving the agent a clear model of what the tool returns.

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?

Two sentences with no wasted words. First sentence explains what the tool does and what it returns. Second sentence gives usage guidance. Highly efficient and front-loaded.

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

Completeness5/5

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

Given the tool's simplicity (single required parameter, safe, idempotent, no output schema), the description covers everything needed: what it reads, what it returns, and when to use it. No omissions.

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?

The schema coverage is 100% (the description for 'path' is present in the schema). The description provides a concrete example ('e.g., 'boards/roadmap.canvas'') but does not add significant new meaning beyond the schema. Baseline 3 aligns with high coverage.

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 verb 'Read' and the resource 'Obsidian canvas file' (specifically .canvas JSON format). It distinguishes itself from sibling tools by mentioning it should be used before calling add_canvas_node or add_canvas_edge.

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

Usage Guidelines4/5

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

The description explicitly says 'Use to inspect or navigate a canvas before calling add_canvas_node or add_canvas_edge,' providing clear context for when this tool is appropriate. It does not explicitly list when not to use it, but the guidance is strong and specific.

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