Skip to main content
Glama
mickm3n

Roam Research MCP Server

by mickm3n

get_page_references

Find all blocks that reference a specific page in Roam Research, with pagination support for large databases. Returns results sorted by recent edits with full hierarchical context.

Instructions

Get all blocks that reference a specific page in Roam Research with pagination support.

Finds all blocks across your Roam database that contain links to the specified page.
Results are sorted by most recent edit time and include the full hierarchical context
of each referencing block.

Args:
    page_name: Exact name of the page to find references for (case-sensitive)
    limit: Maximum number of references to return per request (default: 10)
    cursor: Timestamp cursor for pagination - use next_cursor from previous response
           to get additional results
           
Returns:
    JSON string containing:
    - result: Array of referencing blocks with content and timestamps  
    - next_cursor: Timestamp for pagination (if more results available)
    - total_matches: Number of references found in this batch
    
Examples:
    get_page_references("GTD")
    get_page_references("Project Alpha", limit=20)
    get_page_references("Meeting Notes", limit=5, cursor=1640995200000)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
page_nameYes
limitNo
cursorNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • main.py:419-450 (handler)
    MCP tool handler for 'get_page_references'. Decorated with @mcp.tool(), includes schema in docstring and type hints. Calls internal client method and serializes to JSON.
    @mcp.tool()
    async def get_page_references(page_name: str, limit: int = 10, cursor: Optional[int] = None) -> str:
        """Get all blocks that reference a specific page in Roam Research with pagination support.
        
        Finds all blocks across your Roam database that contain links to the specified page.
        Results are sorted by most recent edit time and include the full hierarchical context
        of each referencing block.
    
        Args:
            page_name: Exact name of the page to find references for (case-sensitive)
            limit: Maximum number of references to return per request (default: 10)
            cursor: Timestamp cursor for pagination - use next_cursor from previous response
                   to get additional results
                   
        Returns:
            JSON string containing:
            - result: Array of referencing blocks with content and timestamps  
            - next_cursor: Timestamp for pagination (if more results available)
            - total_matches: Number of references found in this batch
            
        Examples:
            get_page_references("GTD")
            get_page_references("Project Alpha", limit=20)
            get_page_references("Meeting Notes", limit=5, cursor=1640995200000)
        """
        try:
            client = get_roam_client()
            result = client.get_page_references(page_name, limit, cursor)
            return json.dumps(result, indent=2)
        except Exception as e:
            print(f"Error getting page references: {e}", file=sys.stderr)
            return f"Error: {str(e)}"
  • Core implementation logic in RoamResearchMCPServer class. Executes Datalog queries for page references with pagination support, processes nested block data, converts to markdown hierarchy, and structures the response.
    def get_page_references(self, page_name: str, limit: int = 10, cursor: Optional[int] = None) -> Dict[str, Any]:
        """Get references to a specific page with markdown content and child blocks"""
        # Build the query with time-based sorting and pagination
        if cursor:
            # Use cursor-based pagination for subsequent pages
            query = """[:find (pull ?ref [:block/string
                                          :block/uid
                                          :edit/time
                                          {:block/children [:block/string
                                                            :block/uid
                                                            :edit/time
                                                            {:block/children [:block/string
                                                                              :block/uid
                                                                              :edit/time
                                                                              {:block/children [:block/string
                                                                                                :block/uid
                                                                                                :edit/time
                                                                                                {:block/children [:block/string
                                                                                                                  :block/uid
                                                                                                                  :edit/time]}]}]}]}]) ?time
                        :in $ ?PAGE ?cursor-time
                        :where
                        [?page :node/title ?PAGE]
                        [?ref :block/refs ?page]
                        [?ref :edit/time ?time]
                        [(< ?time ?cursor-time)]
                        ]"""
            data = {"query": query, "args": [page_name, cursor]}
        else:
            # First page - no cursor
            query = """[:find (pull ?ref [:block/string
                                          :block/uid
                                          :edit/time
                                          {:block/children [:block/string
                                                            :block/uid
                                                            :edit/time
                                                            {:block/children [:block/string
                                                                              :block/uid
                                                                              :edit/time
                                                                              {:block/children [:block/string
                                                                                                :block/uid
                                                                                                :edit/time
                                                                                                {:block/children [:block/string
                                                                                                                  :block/uid
                                                                                                                  :edit/time]}]}]}]}]) ?time
                        :in $ ?PAGE
                        :where
                        [?page :node/title ?PAGE]
                        [?ref :block/refs ?page]
                        [?ref :edit/time ?time]
                        ]"""
            data = {"query": query, "args": [page_name]}
    
        endpoint = f"/api/graph/{self.graph_name}/q"
        raw_result = self._make_request("POST", endpoint, data)
    
        # Sort by time (descending) and apply limit
        results = raw_result.get("result", [])
        sorted_results = sorted(results, key=lambda x: x[1], reverse=True)
        limited_results = sorted_results[:limit]
    
        # Transform the result to only include markdown content with children
        simplified_result = []
        next_cursor = None
    
        for item in limited_results:
            if item and len(item) > 0:
                block = item[0]
                timestamp = item[1]
                content = self._build_block_with_children(block)
                simplified_result.append({"content": content, "timestamp": timestamp})
                next_cursor = timestamp
    
        result = {"result": simplified_result}
    
        # Add next_cursor if there are more results
        if len(results) > limit:
            result["next_cursor"] = next_cursor
    
        return result
  • main.py:68-83 (helper)
    Recursive helper to convert Roam block tree to indented markdown representation, preserving hierarchy.
    def _build_block_with_children(self, block: Dict[str, Any]) -> str:
        """Build a markdown string with block content and all its children"""
        content = self._convert_block_to_markdown(block)
    
        # Get children from the nested data structure
        children = block.get(':block/children', [])
        if children:
            content += "\n"
            for child in children:
                child_content = self._build_block_with_children(child)
                # Indent child content
                child_lines = child_content.split('\n')
                indented_lines = ['  ' + line for line in child_lines if line.strip()]
                content += '\n'.join(indented_lines) + '\n'
    
        return content.strip()
  • main.py:33-57 (helper)
    Utility method for making authenticated HTTP requests to Roam Research API.
    def _make_request(
        self, method: str, endpoint: str, data: Optional[Dict] = None
    ) -> Dict[str, Any]:
        """Make HTTP request to Roam Research API"""
        url = f"{self.base_url}{endpoint}"
    
        try:
            if method == "GET":
                response = requests.get(url, headers=self.headers)
            elif method == "POST":
                response = requests.post(url, headers=self.headers, json=data)
            else:
                raise ValueError(f"Unsupported HTTP method: {method}")
    
            response.raise_for_status()
    
            # Handle empty response for write operations
            if response.text.strip() == "":
                return {"result": "success", "status": response.status_code}
    
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}", file=sys.stderr)
            raise
  • Singleton factory for initializing the RoamResearchMCPServer client instance from environment variables.
    def get_roam_client():
        """Get or initialize Roam Research client"""
        global roam_client
    
        if roam_client is None:
            token = os.getenv("ROAM_TOKEN")
            graph_name = os.getenv("ROAM_GRAPH_NAME")
    
            print(f"DEBUG: ROAM_TOKEN present: {bool(token)}", file=sys.stderr)
            print(f"DEBUG: ROAM_GRAPH_NAME present: {bool(graph_name)}", file=sys.stderr)
    
            if not token or not graph_name:
                raise Exception("ROAM_TOKEN and ROAM_GRAPH_NAME environment variables are required")
    
            roam_client = RoamResearchMCPServer(token, graph_name)
            print("DEBUG: Roam client initialized successfully", file=sys.stderr)
    
        return roam_client
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes key behaviors: pagination mechanism (cursor-based), sorting (by most recent edit time), inclusion of hierarchical context, and the JSON return structure. It doesn't mention rate limits, authentication needs, or error conditions, but covers the core operational behavior well.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded with the core purpose. Every sentence adds value: explains what it finds, how results are sorted, parameter details, return format, and examples. No wasted words while maintaining clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (pagination, hierarchical context), no annotations, and the presence of an output schema, the description provides excellent completeness. It explains the pagination mechanism, sorting behavior, return structure, and includes practical examples. The output schema handles return value details, so the description appropriately focuses on operational context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 0%, so the description must fully compensate. It provides excellent parameter semantics: explains page_name is case-sensitive, limit is maximum per request with default, and cursor is for pagination with specific usage instructions. This adds substantial meaning beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Get all blocks that reference a specific page'), resource ('Roam Research'), and scope ('with pagination support'). It distinguishes this from siblings like get_page_content (which retrieves page content rather than references) and write tools by focusing on reference discovery.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context about when to use this tool (finding blocks that link to a specific page) and implies usage through examples. However, it doesn't explicitly state when NOT to use it or name specific alternatives among siblings, though the purpose differentiation is strong enough to guide selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/mickm3n/roam-research-mcp'

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