insert_nodes_from_markdown
Add hierarchical content from indented markdown or bulleted text to Dynalist documents, preserving structure and organization.
Instructions
Insert multiple nodes from indented markdown/text. Supports both '- bullet' format and plain indented text. Preserves hierarchy.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Dynalist URL - document or node (with #z=nodeId) to insert under | |
| content | Yes | Indented text with bullets. Supports '- text' or plain indented text. | |
| position | No | Where to insert under the parent node | as_last_child |
Implementation Reference
- src/tools/index.ts:955-992 (handler)The main handler function that implements the insert_nodes_from_markdown tool logic: parses the Dynalist URL to determine document and parent node, parses the markdown content into a hierarchical tree using parseMarkdownBullets, inserts the entire tree under the parent node using insertTreeUnderParent helper (respecting position), and returns the number of nodes created and URL of the first inserted node.async ({ url, content, position }) => { const parsed = parseDynalistUrl(url); const documentId = parsed.documentId; let parentNodeId = parsed.nodeId; // If no node specified, get root node if (!parentNodeId) { const doc = await client.readDocument(documentId); parentNodeId = findRootNodeId(doc.nodes); } // Parse the markdown content into a tree const tree = parseMarkdownBullets(content); if (tree.length === 0) { return { content: [{ type: "text", text: "No content to insert (empty or invalid format)" }], isError: true, }; } // Use helper to insert tree const result = await insertTreeUnderParent(client, documentId, parentNodeId, tree, { startIndex: position === "as_first_child" ? 0 : undefined, }); const firstNodeUrl = result.rootNodeIds.length > 0 ? buildDynalistUrl(documentId, result.rootNodeIds[0]) : ""; return { content: [ { type: "text", text: `Successfully inserted ${result.totalCreated} nodes!\nFirst node: ${firstNodeUrl}`, }, ], }; }
- src/tools/index.ts:949-954 (schema)Zod input schema defining parameters: url (Dynalist document/node URL), content (indented markdown/text), position (optional: 'as_first_child' or 'as_last_child', defaults to last).{ url: z.string().describe("Dynalist URL - document or node (with #z=nodeId) to insert under"), content: z.string().describe("Indented text with bullets. Supports '- text' or plain indented text."), position: z.enum(["as_first_child", "as_last_child"]).optional().default("as_last_child") .describe("Where to insert under the parent node"), },
- src/tools/index.ts:946-993 (registration)MCP server tool registration call for 'insert_nodes_from_markdown', including name, description, schema, and inline handler function.server.tool( "insert_nodes_from_markdown", "Insert multiple nodes from indented markdown/text. Supports both '- bullet' format and plain indented text. Preserves hierarchy.", { url: z.string().describe("Dynalist URL - document or node (with #z=nodeId) to insert under"), content: z.string().describe("Indented text with bullets. Supports '- text' or plain indented text."), position: z.enum(["as_first_child", "as_last_child"]).optional().default("as_last_child") .describe("Where to insert under the parent node"), }, async ({ url, content, position }) => { const parsed = parseDynalistUrl(url); const documentId = parsed.documentId; let parentNodeId = parsed.nodeId; // If no node specified, get root node if (!parentNodeId) { const doc = await client.readDocument(documentId); parentNodeId = findRootNodeId(doc.nodes); } // Parse the markdown content into a tree const tree = parseMarkdownBullets(content); if (tree.length === 0) { return { content: [{ type: "text", text: "No content to insert (empty or invalid format)" }], isError: true, }; } // Use helper to insert tree const result = await insertTreeUnderParent(client, documentId, parentNodeId, tree, { startIndex: position === "as_first_child" ? 0 : undefined, }); const firstNodeUrl = result.rootNodeIds.length > 0 ? buildDynalistUrl(documentId, result.rootNodeIds[0]) : ""; return { content: [ { type: "text", text: `Successfully inserted ${result.totalCreated} nodes!\nFirst node: ${firstNodeUrl}`, }, ], }; } );
- src/utils/markdown-parser.ts:19-57 (helper)Core helper that parses indented markdown or plain text into a hierarchical ParsedNode[] tree structure, detecting indentation levels, stripping bullet markers, and building the tree using stack-based algorithm. Essential for processing the 'content' input.export function parseMarkdownBullets(text: string): ParsedNode[] { const lines = text.split("\n"); const flatNodes: FlatNode[] = []; // Detect indent unit (smallest non-zero indent) let indentUnit = 4; // default for (const line of lines) { if (!line.trim()) continue; const leadingSpaces = getLeadingSpaces(line); if (leadingSpaces > 0 && leadingSpaces < indentUnit) { indentUnit = leadingSpaces; } } // Parse each line into flat nodes with levels for (const line of lines) { // Skip empty lines if (!line.trim()) continue; const leadingSpaces = getLeadingSpaces(line); const level = Math.floor(leadingSpaces / indentUnit); // Extract content - remove leading whitespace and bullet marker let content = line.trim(); // Remove bullet markers: "- ", "* ", "• ", numbered "1. ", "1) " content = content .replace(/^[-*•]\s+/, "") // - text, * text, • text .replace(/^\d+[.)]\s+/, "") // 1. text, 1) text .replace(/^>\s*/, ""); // > quote (treat as regular text) if (content) { flatNodes.push({ content, level }); } } // Convert flat nodes to tree return buildTree(flatNodes); }
- src/tools/index.ts:95-148 (helper)Supporting helper function that inserts an entire ParsedNode tree under a given parent by processing level-by-level: groups nodes by level using groupByLevel, batches insert changes per level via Dynalist editDocument API, tracks new node IDs for child levels, supports startIndex for position and checkbox option. Returns stats and root node IDs.async function insertTreeUnderParent( client: DynalistClient, fileId: string, parentId: string, tree: ParsedNode[], options: { startIndex?: number; checkbox?: boolean } = {} ): Promise<{ totalCreated: number; rootNodeIds: string[] }> { if (tree.length === 0) { return { totalCreated: 0, rootNodeIds: [] }; } const levels = groupByLevel(tree); let totalCreated = 0; let rootNodeIds: string[] = []; let previousLevelIds: string[] = []; for (let levelIdx = 0; levelIdx < levels.length; levelIdx++) { const level = levels[levelIdx]; const changes: { action: string; parent_id: string; index: number; content: string; checkbox?: boolean }[] = []; const childCountPerParent = new Map<string, number>(); for (const node of level) { const nodeParentId = node.parentLevelIndex === -1 ? parentId : previousLevelIds[node.parentLevelIndex]; const baseIndex = (levelIdx === 0 && options.startIndex !== undefined) ? options.startIndex : 0; const count = childCountPerParent.get(nodeParentId) || 0; changes.push({ action: "insert", parent_id: nodeParentId, index: baseIndex + count, content: node.content, checkbox: options.checkbox || undefined, }); childCountPerParent.set(nodeParentId, count + 1); } const response = await client.editDocument(fileId, changes as any); const newIds = response.new_node_ids || []; if (levelIdx === 0) { rootNodeIds = newIds; } totalCreated += newIds.length; previousLevelIds = newIds; } return { totalCreated, rootNodeIds }; }