read_node_as_markdown
Convert Dynalist documents or specific nodes into Markdown format for easier editing, sharing, or integration with other tools. Control output size with depth limits and optional content filters.
Instructions
Read a Dynalist document or specific node and return it as Markdown. Provide either a URL (with optional #z=nodeId deep link) or file_id + node_id. WARNING: Large documents may return many words - use max_depth to limit.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | Dynalist URL (e.g., https://dynalist.io/d/xxx#z=yyy) | |
| file_id | No | Document ID (alternative to URL) | |
| node_id | No | Node ID to start from (optional, reads entire doc if not provided) | |
| max_depth | No | Maximum depth to traverse (optional, unlimited if not set) - USE THIS TO LIMIT OUTPUT SIZE | |
| include_notes | No | Include notes as sub-bullets | |
| include_checked | No | Include checked/completed items | |
| bypass_warning | No | ONLY use after receiving a size warning. Do NOT set true on first request. |
Implementation Reference
- src/tools/index.ts:249-313 (handler)The main handler function that orchestrates the tool execution: parses input URL or file_id/node_id, fetches the Dynalist document, converts the node or entire document to Markdown using helper functions, checks output size, and returns the Markdown text or a size warning.async ({ url, file_id, node_id, max_depth, include_notes, include_checked, bypass_warning }) => { // Parse URL if provided let documentId = file_id; let nodeId = node_id; if (url) { const parsed = parseDynalistUrl(url); documentId = parsed.documentId; nodeId = nodeId || parsed.nodeId; } if (!documentId) { return { content: [{ type: "text", text: "Error: Either 'url' or 'file_id' must be provided" }], isError: true, }; } // Fetch document const doc = await client.readDocument(documentId); const nodeMap = buildNodeMap(doc.nodes); const options = { maxDepth: max_depth, includeNotes: include_notes, includeChecked: include_checked, }; let markdown: string; if (nodeId) { // Render from specific node if (!nodeMap.has(nodeId)) { return { content: [{ type: "text", text: `Error: Node '${nodeId}' not found in document` }], isError: true, }; } markdown = nodeToMarkdown(doc.nodes, nodeId, options); } else { // Render entire document markdown = documentToMarkdown(doc.nodes, options); } // Check content size const sizeCheck = checkContentSize(markdown, bypass_warning || false, [ "Use max_depth to limit traversal depth (e.g., max_depth: 2)", "Target a specific node_id instead of entire document", ]); if (sizeCheck) { return { content: [{ type: "text", text: sizeCheck.warning }], }; } return { content: [ { type: "text", text: markdown.trim(), }, ], }; }
- src/tools/index.ts:240-248 (schema)Zod input schema defining parameters for the tool: URL or file_id/node_id, max_depth for limiting size, toggles for notes/checked items, and bypass_warning for large outputs.{ url: z.string().optional().describe("Dynalist URL (e.g., https://dynalist.io/d/xxx#z=yyy)"), file_id: z.string().optional().describe("Document ID (alternative to URL)"), node_id: z.string().optional().describe("Node ID to start from (optional, reads entire doc if not provided)"), max_depth: z.number().optional().describe("Maximum depth to traverse (optional, unlimited if not set) - USE THIS TO LIMIT OUTPUT SIZE"), include_notes: z.boolean().optional().default(true).describe("Include notes as sub-bullets"), include_checked: z.boolean().optional().default(true).describe("Include checked/completed items"), bypass_warning: z.boolean().optional().default(false).describe("ONLY use after receiving a size warning. Do NOT set true on first request."), },
- src/tools/index.ts:237-314 (registration)Registration of the read_node_as_markdown tool with the MCP server using server.tool(), including name, description, input schema, and handler function.server.tool( "read_node_as_markdown", "Read a Dynalist document or specific node and return it as Markdown. Provide either a URL (with optional #z=nodeId deep link) or file_id + node_id. WARNING: Large documents may return many words - use max_depth to limit.", { url: z.string().optional().describe("Dynalist URL (e.g., https://dynalist.io/d/xxx#z=yyy)"), file_id: z.string().optional().describe("Document ID (alternative to URL)"), node_id: z.string().optional().describe("Node ID to start from (optional, reads entire doc if not provided)"), max_depth: z.number().optional().describe("Maximum depth to traverse (optional, unlimited if not set) - USE THIS TO LIMIT OUTPUT SIZE"), include_notes: z.boolean().optional().default(true).describe("Include notes as sub-bullets"), include_checked: z.boolean().optional().default(true).describe("Include checked/completed items"), bypass_warning: z.boolean().optional().default(false).describe("ONLY use after receiving a size warning. Do NOT set true on first request."), }, async ({ url, file_id, node_id, max_depth, include_notes, include_checked, bypass_warning }) => { // Parse URL if provided let documentId = file_id; let nodeId = node_id; if (url) { const parsed = parseDynalistUrl(url); documentId = parsed.documentId; nodeId = nodeId || parsed.nodeId; } if (!documentId) { return { content: [{ type: "text", text: "Error: Either 'url' or 'file_id' must be provided" }], isError: true, }; } // Fetch document const doc = await client.readDocument(documentId); const nodeMap = buildNodeMap(doc.nodes); const options = { maxDepth: max_depth, includeNotes: include_notes, includeChecked: include_checked, }; let markdown: string; if (nodeId) { // Render from specific node if (!nodeMap.has(nodeId)) { return { content: [{ type: "text", text: `Error: Node '${nodeId}' not found in document` }], isError: true, }; } markdown = nodeToMarkdown(doc.nodes, nodeId, options); } else { // Render entire document markdown = documentToMarkdown(doc.nodes, options); } // Check content size const sizeCheck = checkContentSize(markdown, bypass_warning || false, [ "Use max_depth to limit traversal depth (e.g., max_depth: 2)", "Target a specific node_id instead of entire document", ]); if (sizeCheck) { return { content: [{ type: "text", text: sizeCheck.warning }], }; } return { content: [ { type: "text", text: markdown.trim(), }, ], }; } );
- src/utils/node-to-markdown.ts:42-89 (helper)Core recursive helper function renderNode that traverses the node tree, formats bullets with checkboxes/headings, includes notes as sub-bullets, respects maxDepth and includeChecked options, and builds the Markdown string.function renderNode( nodeMap: Map<string, DynalistNode>, nodeId: string, depth: number, options: Required<ConvertOptions> ): string { const node = nodeMap.get(nodeId); if (!node) return ""; // Skip checked items if option is disabled if (!options.includeChecked && node.checked) { return ""; } // Check max depth if (depth > options.maxDepth) { return ""; } const indent = options.indent.repeat(depth); let result = ""; // Build the bullet line const bullet = formatBullet(node); const content = formatContent(node); const children = node.children || []; if (content || children.length > 0) { result += `${indent}${bullet}${content}\n`; } // Add note as sub-bullet if present and enabled if (options.includeNotes && node.note && node.note.trim()) { const noteIndent = options.indent.repeat(depth + 1); // Split note by newlines and render each as a sub-bullet const noteLines = node.note.split("\n").filter((line) => line.trim()); for (const noteLine of noteLines) { result += `${noteIndent}- ${noteLine.trim()}\n`; } } // Render children for (const childId of children) { result += renderNode(nodeMap, childId, depth + 1, options); } return result; }
- Helper function to convert an entire Dynalist document to Markdown by finding the root node and rendering its top-level children using renderNode.export function documentToMarkdown( nodes: DynalistNode[], options: ConvertOptions = {} ): string { const nodeMap = buildNodeMap(nodes); // Find root node (typically the first node, which represents the document) // The root node's children are the top-level items const rootNode = nodes.find((n) => { // Root is the node not referenced as child by anyone const isChild = nodes.some((other) => (other.children || []).includes(n.id)); return !isChild; }); if (!rootNode) { return ""; } const opts = { ...DEFAULT_OPTIONS, ...options }; let result = ""; // Render children of root (not the root itself, which is usually empty) for (const childId of rootNode.children || []) { result += renderNode(nodeMap, childId, 0, opts); } return result; }