Skip to main content
Glama
camiloluvino

Roam Research MCP Server

by camiloluvino

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
NameRequiredDescriptionDefault
titleYesTitle of the page. For date pages, use ordinal date formats such as January 2nd, 2025
formatNoFormat output as markdown or JSON. 'markdown' returns as string; 'raw' returns JSON string of the page's blocksraw

Implementation Reference

  • 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)}`; }
  • 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'] }, },
  • 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 }], }; }
  • 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); }
  • 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',

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