Skip to main content
Glama
camiloluvino

Roam Research MCP Server

by camiloluvino

roam_recall

Retrieve and organize stored memories from Roam Research pages or tagged blocks, with options to filter by specific tags and sort by creation date.

Instructions

Retrieve all stored memories on page titled MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blocks with a specific tag and sort by creation date.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
sort_byNoSort order for memories based on creation datenewest
filter_tagNoInclude only memories with a specific filter tag. For single word tags use format "tag", for multi-word tags use format "tag word" (without brackets)

Implementation Reference

  • Core implementation of the roam_recall tool handler. Queries the memories page and graph for blocks tagged with MEMORIES_TAG, sorts by creation time, resolves block references, deduplicates, optionally filters by tag, removes the memories tag prefix, and returns the list of memories.
    async recall(sort_by: 'newest' | 'oldest' = 'newest', filter_tag?: string): Promise<{ success: boolean; memories: string[] }> { // Get memories tag from environment var memoriesTag = process.env.MEMORIES_TAG; if (!memoriesTag) { memoriesTag = "Memories" } // Extract the tag text, removing any formatting const tagText = memoriesTag .replace(/^#/, '') // Remove leading # .replace(/^\[\[/, '').replace(/\]\]$/, ''); // Remove [[ and ]] try { // Get page blocks using query to access actual block content const ancestorRule = `[ [ (ancestor ?b ?a) [?a :block/children ?b] ] [ (ancestor ?b ?a) [?parent :block/children ?b] (ancestor ?parent ?a) ] ]`; // Query to find all blocks on the page const pageQuery = `[:find ?string ?time :in $ % ?title :where [?page :node/title ?title] [?block :block/string ?string] [?block :create/time ?time] (ancestor ?block ?page)]`; // Execute query const pageResults = await q(this.graph, pageQuery, [ancestorRule, tagText]) as [string, number][]; // Process page blocks with sorting let pageMemories = pageResults .sort(([_, aTime], [__, bTime]) => sort_by === 'newest' ? bTime - aTime : aTime - bTime ) .map(([content]) => content); // Get tagged blocks from across the graph const tagResults = await this.searchOps.searchForTag(tagText); // Process tagged blocks with sorting let taggedMemories = tagResults.matches .sort((a: SearchResult, b: SearchResult) => { const aTime = a.block_uid ? parseInt(a.block_uid.split('-')[0], 16) : 0; const bTime = b.block_uid ? parseInt(b.block_uid.split('-')[0], 16) : 0; return sort_by === 'newest' ? bTime - aTime : aTime - bTime; }) .map(match => match.content); // Resolve any block references in both sets const resolvedPageMemories = await Promise.all( pageMemories.map(async (content: string) => resolveRefs(this.graph, content)) ); const resolvedTaggedMemories = await Promise.all( taggedMemories.map(async (content: string) => resolveRefs(this.graph, content)) ); // Combine both sets and remove duplicates while preserving order let uniqueMemories = [ ...resolvedPageMemories, ...resolvedTaggedMemories ].filter((memory, index, self) => self.indexOf(memory) === index ); // Format filter tag with exact Roam tag syntax const filterTagFormatted = filter_tag ? (filter_tag.includes(' ') ? `#[[${filter_tag}]]` : `#${filter_tag}`) : null; // Filter by exact tag match if provided if (filterTagFormatted) { uniqueMemories = uniqueMemories.filter(memory => memory.includes(filterTagFormatted)); } // Format memories tag for removal and clean up memories tag const memoriesTagFormatted = tagText.includes(' ') || tagText.includes('/') ? `#[[${tagText}]]` : `#${tagText}`; uniqueMemories = uniqueMemories.map(memory => memory.replace(memoriesTagFormatted, '').trim()); // return { // success: true, // memories: [ // `memoriesTag = ${memoriesTag}`, // `filter_tag = ${filter_tag}`, // `filterTagFormatted = ${filterTagFormatted}`, // `memoriesTagFormatted = ${memoriesTagFormatted}`, // ] // } return { success: true, memories: uniqueMemories }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to recall memories: ${error.message}` ); } }
  • Input schema definition for the roam_recall tool, including parameters for sorting and filtering memories.
    [toolName(BASE_TOOL_NAMES.RECALL)]: { name: toolName(BASE_TOOL_NAMES.RECALL), description: 'Retrieve all stored memories on page titled MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blocks with a specific tag and sort by creation date.', inputSchema: { type: 'object', properties: { sort_by: { type: 'string', description: 'Sort order for memories based on creation date', enum: ['newest', 'oldest'], default: 'newest' }, filter_tag: { type: 'string', description: 'Include only memories with a specific filter tag. For single word tags use format "tag", for multi-word tags use format "tag word" (without brackets)' } } } },
  • Registration of the roam_recall tool in the MCP server switch statement, dispatching to ToolHandlers.recall().
    case BASE_TOOL_NAMES.RECALL: { const { sort_by = 'newest', filter_tag } = request.params.arguments as { sort_by?: 'newest' | 'oldest'; filter_tag?: string; }; const result = await this.toolHandlers.recall(sort_by, filter_tag); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; }
  • Wrapper handler method in ToolHandlers class that delegates to MemoryOperations.recall()
    async recall(sort_by: 'newest' | 'oldest' = 'newest', filter_tag?: string) { return this.memoryOps.recall(sort_by, filter_tag); }
  • Base tool name constant used for schema key and registration matching.
    RECALL: 'roam_recall',

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