roam_fetch_page_by_title
Retrieve Roam Research page content by title in markdown or JSON format for accessing knowledge graph information.
Instructions
Fetch page by title. Returns content in the specified format.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025 | |
| format | No | Format output as markdown or JSON. 'markdown' returns as string; 'raw' returns JSON string of the page's blocks | raw |
Implementation Reference
- src/tools/operations/pages.ts:224-376 (handler)The main handler function that queries the Roam graph for the page by title (with case variations), retrieves all descendant blocks, resolves references, builds the hierarchical structure of blocks, and returns either raw JSON array of blocks or formatted Markdown.async fetchPageByTitle( title: string, format: 'markdown' | 'raw' = 'raw' ): Promise<string | RoamBlock[]> { if (!title) { throw new McpError(ErrorCode.InvalidRequest, 'title is required'); } // Try different case variations // Generate variations to check const variations = [ title, // Original capitalizeWords(title), // Each word capitalized title.toLowerCase() // All lowercase ]; // Create OR clause for query const orClause = variations.map(v => `[?e :node/title "${v}"]`).join(' '); const searchQuery = `[:find ?uid . :where [?e :block/uid ?uid] (or ${orClause})]`; const result = await q(this.graph, searchQuery, []); const uid = (result === null || result === undefined) ? null : String(result); if (!uid) { throw new McpError( ErrorCode.InvalidRequest, `Page with title "${title}" not found (tried original, capitalized words, and lowercase)` ); } // Define ancestor rule for traversing block hierarchy const ancestorRule = `[ [ (ancestor ?b ?a) [?a :block/children ?b] ] [ (ancestor ?b ?a) [?parent :block/children ?b] (ancestor ?parent ?a) ] ]`; // Get all blocks under this page using ancestor rule const blocksQuery = `[:find ?block-uid ?block-str ?order ?parent-uid :in $ % ?page-title :where [?page :node/title ?page-title] [?block :block/string ?block-str] [?block :block/uid ?block-uid] [?block :block/order ?order] (ancestor ?block ?page) [?parent :block/children ?block] [?parent :block/uid ?parent-uid]]`; const blocks = await q(this.graph, blocksQuery, [ancestorRule, title]); if (!blocks || blocks.length === 0) { if (format === 'raw') { return []; } return `${title} (no content found)`; } // Get heading information for blocks that have it const headingsQuery = `[:find ?block-uid ?heading :in $ % ?page-title :where [?page :node/title ?page-title] [?block :block/uid ?block-uid] [?block :block/heading ?heading] (ancestor ?block ?page)]`; const headings = await q(this.graph, headingsQuery, [ancestorRule, title]); // Create a map of block UIDs to heading levels const headingMap = new Map<string, number>(); if (headings) { for (const [blockUid, heading] of headings) { headingMap.set(blockUid, heading as number); } } // Create a map of all blocks const blockMap = new Map<string, RoamBlock>(); const rootBlocks: RoamBlock[] = []; // First pass: Create all block objects for (const [blockUid, blockStr, order, parentUid] of blocks) { const resolvedString = await resolveRefs(this.graph, blockStr); const block = { uid: blockUid, string: resolvedString, order: order as number, heading: headingMap.get(blockUid) || null, children: [] }; blockMap.set(blockUid, block); // If no parent or parent is the page itself, it's a root block if (!parentUid || parentUid === uid) { rootBlocks.push(block); } } // Second pass: Build parent-child relationships for (const [blockUid, _, __, parentUid] of blocks) { if (parentUid && parentUid !== uid) { const child = blockMap.get(blockUid); const parent = blockMap.get(parentUid); if (child && parent && !parent.children.includes(child)) { parent.children.push(child); } } } // Sort blocks recursively const sortBlocks = (blocks: RoamBlock[]) => { blocks.sort((a, b) => a.order - b.order); blocks.forEach(block => { if (block.children.length > 0) { sortBlocks(block.children); } }); }; sortBlocks(rootBlocks); if (format === 'raw') { return JSON.stringify(rootBlocks); } // Convert to markdown with proper nesting const toMarkdown = (blocks: RoamBlock[], level: number = 0): string => { return blocks .map(block => { const indent = ' '.repeat(level); let md: string; // Check block heading level and format accordingly if (block.heading && block.heading > 0) { // Format as heading with appropriate number of hashtags const hashtags = '#'.repeat(block.heading); md = `${indent}${hashtags} ${block.string}`; } else { // No heading, use bullet point (current behavior) md = `${indent}- ${block.string}`; } if (block.children.length > 0) { md += '\n' + toMarkdown(block.children, level + 1); } return md; }) .join('\n'); }; return `# ${title}\n\n${toMarkdown(rootBlocks)}`; }
- src/tools/schemas.ts:58-79 (schema)Defines the tool schema including name, description, and input schema (title required, format optional: 'markdown' or 'raw').[toolName(BASE_TOOL_NAMES.FETCH_PAGE_BY_TITLE)]: { name: toolName(BASE_TOOL_NAMES.FETCH_PAGE_BY_TITLE), description: 'Fetch page by title. Returns content in the specified format.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025' }, format: { type: 'string', enum: ['markdown', 'raw'], default: 'raw', description: "Format output as markdown or JSON. 'markdown' returns as string; 'raw' returns JSON string of the page's blocks" } }, required: ['title'] }, },
- src/server/roam-server.ts:134-143 (registration)Tool registration in the MCP server request handler switch statement, dispatching tool calls to ToolHandlers.fetchPageByTitle.case BASE_TOOL_NAMES.FETCH_PAGE_BY_TITLE: { const { title, format } = request.params.arguments as { title: string; format?: 'markdown' | 'raw'; }; const content = await this.toolHandlers.fetchPageByTitle(title, format); return { content: [{ type: 'text', text: content }], }; }
- src/tools/tool-handlers.ts:46-48 (helper)Facade method in ToolHandlers class that delegates the fetchPageByTitle call to PageOperations instance.async fetchPageByTitle(title: string, format?: 'markdown' | 'raw') { return this.pageOps.fetchPageByTitle(title, format); }
- src/tools/schemas.ts:10-10 (registration)Constant definition mapping FETCH_PAGE_BY_TITLE to the tool name 'roam_fetch_page_by_title' used throughout the codebase.FETCH_PAGE_BY_TITLE: 'roam_fetch_page_by_title',