roam_import_markdown
Import nested markdown content into Roam Research by adding it under a specific block, creating parent blocks when needed for structured knowledge organization.
Instructions
Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a parent_string is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.
IMPORTANT: Before using this tool, ensure that you have loaded into context the 'Roam Markdown Cheatsheet' resource.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| content | Yes | Nested markdown content to import | |
| page_uid | No | Optional: UID of the page containing the parent block (preferred for accuracy). | |
| page_title | No | Optional: Title of the page containing the parent block (used if page_uid is not provided). | |
| parent_uid | No | Optional: UID of the parent block to add content under (preferred for accuracy). | |
| parent_string | No | Optional: Exact string content of an existing parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title). If the block does not exist, it will be created. | |
| order | No | Optional: Where to add the content under the parent ("first" or "last") | first |
Implementation Reference
- src/tools/operations/outline.ts:497-671 (handler)Core handler implementation that resolves target page and parent block, parses nested markdown content using parseMarkdown and convertToRoamActions, executes batchActions to import into Roam graph, fetches and returns created nested block structure.async importMarkdown( content: string, page_uid?: string, page_title?: string, parent_uid?: string, parent_string?: string, order: 'first' | 'last' = 'last' ): Promise<{ success: boolean; page_uid: string; parent_uid: string; created_uids: NestedBlock[] }> { // First get the page UID let targetPageUid = page_uid; if (!targetPageUid && page_title) { const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; const findResults = await q(this.graph, findQuery, [page_title]) as [string][]; if (findResults && findResults.length > 0) { targetPageUid = findResults[0][0]; } else { throw new McpError( ErrorCode.InvalidRequest, `Page with title "${page_title}" not found` ); } } // If no page specified, use today's date page if (!targetPageUid) { const today = new Date(); const dateStr = formatRoamDate(today); const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; const findResults = await q(this.graph, findQuery, [dateStr]) as [string][]; if (findResults && findResults.length > 0) { targetPageUid = findResults[0][0]; } else { // Create today's page try { await createPage(this.graph, { action: 'create-page', page: { title: dateStr } }); const results = await q(this.graph, findQuery, [dateStr]) as [string][]; if (!results || results.length === 0) { throw new McpError( ErrorCode.InternalError, 'Could not find created today\'s page' ); } targetPageUid = results[0][0]; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}` ); } } } // Now get the parent block UID let targetParentUid = parent_uid; if (!targetParentUid && parent_string) { if (!targetPageUid) { throw new McpError( ErrorCode.InvalidRequest, 'Must provide either page_uid or page_title when using parent_string' ); } // Find block by exact string match within the page const findBlockQuery = `[:find ?b-uid :in $ ?page-uid ?block-string :where [?p :block/uid ?page-uid] [?b :block/page ?p] [?b :block/string ?block-string] [?b :block/uid ?b-uid]]`; const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, parent_string]) as [string][]; if (blockResults && blockResults.length > 0) { targetParentUid = blockResults[0][0]; } else { // If parent_string block doesn't exist, create it targetParentUid = await this.createAndVerifyBlock(parent_string, targetPageUid); } } // If no parent specified, use page as parent if (!targetParentUid) { targetParentUid = targetPageUid; } // Always use parseMarkdown for content with multiple lines or any markdown formatting const isMultilined = content.includes('\n'); if (isMultilined) { // Parse markdown into hierarchical structure const convertedContent = convertToRoamMarkdown(content); const nodes = parseMarkdown(convertedContent); // Convert markdown nodes to batch actions const actions = convertToRoamActions(nodes, targetParentUid, order); // Execute batch actions to add content const result = await batchActions(this.graph, { action: 'batch-actions', actions }); if (!result) { throw new McpError( ErrorCode.InternalError, 'Failed to import nested markdown content' ); } // After successful batch action, get all nested UIDs under the parent const createdUids = await this.fetchNestedStructure(targetParentUid); return { success: true, page_uid: targetPageUid, parent_uid: targetParentUid, created_uids: createdUids }; } else { // Create a simple block for non-nested content using batchActions const actions = [{ action: 'create-block', location: { "parent-uid": targetParentUid, "order": order }, block: { string: content } }]; try { await batchActions(this.graph, { action: 'batch-actions', actions }); } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create content block: ${error instanceof Error ? error.message : String(error)}` ); } // For single-line content, we still need to fetch the UID and construct a NestedBlock const createdUids: NestedBlock[] = []; try { const foundUid = await this.findBlockWithRetry(targetParentUid, content); if (foundUid) { createdUids.push({ uid: foundUid, text: content, level: 0, order: 0, children: [] }); } } catch (error: any) { // Log warning but don't re-throw, as the block might be created, just not immediately verifiable // console.warn(`Could not verify single block creation for "${content}": ${error.message}`); } return { success: true, page_uid: targetPageUid, parent_uid: targetParentUid, created_uids: createdUids }; } }
- src/tools/schemas.ts:169-204 (schema)Input schema definition, description, and tool name generation for 'roam_import_markdown'.[toolName(BASE_TOOL_NAMES.IMPORT_MARKDOWN)]: { name: toolName(BASE_TOOL_NAMES.IMPORT_MARKDOWN), description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a `parent_string` is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Nested markdown content to import' }, page_uid: { type: 'string', description: 'Optional: UID of the page containing the parent block (preferred for accuracy).' }, page_title: { type: 'string', description: 'Optional: Title of the page containing the parent block (used if page_uid is not provided).' }, parent_uid: { type: 'string', description: 'Optional: UID of the parent block to add content under (preferred for accuracy).' }, parent_string: { type: 'string', description: 'Optional: Exact string content of an existing parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title). If the block does not exist, it will be created.' }, order: { type: 'string', description: 'Optional: Where to add the content under the parent ("first" or "last")', enum: ['first', 'last'], default: 'first' } }, required: ['content'] } },
- src/server/roam-server.ts:160-187 (registration)MCP server request handler registration for the tool, extracting arguments and delegating to ToolHandlers.importMarkdown.case BASE_TOOL_NAMES.IMPORT_MARKDOWN: { const { content, page_uid, page_title, parent_uid, parent_string, order = 'first' } = request.params.arguments as { content: string; page_uid?: string; page_title?: string; parent_uid?: string; parent_string?: string; order?: 'first' | 'last'; }; const result = await this.toolHandlers.importMarkdown( content, page_uid, page_title, parent_uid, parent_string, order ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; }
- src/tools/tool-handlers.ts:128-137 (handler)Wrapper handler in ToolHandlers class that delegates to OutlineOperations.importMarkdown.async importMarkdown( content: string, page_uid?: string, page_title?: string, parent_uid?: string, parent_string?: string, order: 'first' | 'last' = 'first' ) { return this.outlineOps.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order); }