Skip to main content
Glama
camiloluvino

Roam Research MCP Server

by camiloluvino

roam_create_page

Create a new standalone page in Roam Research with structured outlines using explicit nesting levels and headings (H1-H3). This tool establishes foundational concept pages, new topic areas, reference materials, or permanent information collections in a single step.

Instructions

Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:

  • Creating foundational concept pages that other pages will link to/from

  • Establishing new topic areas that need their own namespace

  • Setting up reference materials or documentation

  • Making permanent collections of information. IMPORTANT: Before using this tool, ensure that you have loaded into context the 'Roam Markdown Cheatsheet' resource.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
titleYesTitle of the new page
contentNoInitial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation.

Implementation Reference

  • Core handler function that creates a new Roam page (or uses existing), adds structured content via batch actions using convertToRoamActions, and optionally links it to the daily page.
    async createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>): Promise<{ success: boolean; uid: string }> { // Ensure title is properly formatted const pageTitle = String(title).trim(); // First try to find if the page exists const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; type FindResult = [string]; const findResults = await q(this.graph, findQuery, [pageTitle]) as FindResult[]; let pageUid: string | undefined; if (findResults && findResults.length > 0) { // Page exists, use its UID pageUid = findResults[0][0]; } else { // Create new page try { await createRoamPage(this.graph, { action: 'create-page', page: { title: pageTitle } }); // Get the new page's UID const results = await q(this.graph, findQuery, [pageTitle]) as FindResult[]; if (!results || results.length === 0) { throw new Error('Could not find created page'); } pageUid = results[0][0]; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create page: ${error instanceof Error ? error.message : String(error)}` ); } } // If content is provided, create blocks using batch operations if (content && content.length > 0) { try { // Convert content array to MarkdownNode format expected by convertToRoamActions const nodes = content.map(block => ({ content: convertToRoamMarkdown(block.text.replace(/^#+\s*/, '')), level: block.level, ...(block.heading && { heading_level: block.heading }), children: [] })); // Create hierarchical structure based on levels const rootNodes: any[] = []; const levelMap: { [level: number]: any } = {}; for (const node of nodes) { if (node.level === 1) { rootNodes.push(node); levelMap[1] = node; } else { const parentLevel = node.level - 1; const parent = levelMap[parentLevel]; if (!parent) { throw new Error(`Invalid block hierarchy: level ${node.level} block has no parent`); } parent.children.push(node); levelMap[node.level] = node; } } // Generate batch actions for all blocks const actions = convertToRoamActions(rootNodes, pageUid, 'last'); // Execute batch operation if (actions.length > 0) { const batchResult = await batchActions(this.graph, { action: 'batch-actions', actions }); if (!batchResult) { throw new Error('Failed to create blocks'); } } } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to add content to page: ${error instanceof Error ? error.message : String(error)}` ); } } // Add a link to the created page on today's daily page try { const today = new Date(); const day = today.getDate(); const month = today.toLocaleString('en-US', { month: 'long' }); const year = today.getFullYear(); const formattedTodayTitle = `${month} ${day}${getOrdinalSuffix(day)}, ${year}`; const dailyPageQuery = `[:find ?uid . :where [?e :node/title "${formattedTodayTitle}"] [?e :block/uid ?uid]]`; const dailyPageResult = await q(this.graph, dailyPageQuery, []); const dailyPageUid = dailyPageResult ? String(dailyPageResult) : null; if (dailyPageUid) { await createBlock(this.graph, { action: 'create-block', block: { string: `Created page: [[${pageTitle}]]` }, location: { 'parent-uid': dailyPageUid, order: 'last' } }); } else { console.warn(`Could not find daily page with title: ${formattedTodayTitle}. Link to created page not added.`); } } catch (error) { console.error(`Failed to add link to daily page: ${error instanceof Error ? error.message : String(error)}`); } return { success: true, uid: pageUid }; }
  • Registers the 'roam_create_page' tool in the MCP server switch statement, extracting arguments and calling ToolHandlers.createPage
    case BASE_TOOL_NAMES.CREATE_PAGE: { const { title, content } = request.params.arguments as { title: string; content?: Array<{ text: string; level: number; }>; }; const result = await this.toolHandlers.createPage(title, content); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; }
  • Defines the tool schema including name 'roam_create_page', description, and inputSchema for title and optional content array of blocks.
    [toolName(BASE_TOOL_NAMES.CREATE_PAGE)]: { name: toolName(BASE_TOOL_NAMES.CREATE_PAGE), description: 'Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the new page', }, content: { type: 'array', description: 'Initial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation.', items: { type: 'object', properties: { text: { type: 'string', description: 'Content of the block' }, level: { type: 'integer', description: 'Indentation level (1-10, where 1 is top level)', minimum: 1, maximum: 10 }, heading: { type: 'integer', description: 'Optional: Heading formatting for this block (1-3)', minimum: 1, maximum: 3 } }, required: ['text', 'level'] } }, }, required: ['title'], }, },
  • ToolHandlers wrapper method that delegates to PageOperations.createPage
    async createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>) { return this.pageOps.createPage(title, content); }
  • TypeScript interface definition for RoamCreatePage used in SDK calls.
    interface RoamCreatePage { action?: 'create-page'; page: { title: string; uid?: string; 'children-view-type'?: string; }; }

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/camiloluvino/roamMCP'

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