Skip to main content
Glama

read_note

Retrieve markdown notes by title or permalink from your local knowledge base to access stored information and continue conversations with context.

Instructions

Read a markdown note by title or permalink.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
identifierYes
projectNo
pageNo
page_sizeNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Core handler implementation for the 'read_note' MCP tool. Resolves project, validates identifier, fetches note content via REST API using entity ID, with fallbacks to title and text search, and returns helpful guidance if no exact match.
    @mcp.tool(
        description="Read a markdown note by title or permalink.",
    )
    async def read_note(
        identifier: str,
        project: Optional[str] = None,
        page: int = 1,
        page_size: int = 10,
        context: Context | None = None,
    ) -> str:
        """Return the raw markdown for a note, or guidance text if no match is found.
    
        Finds and retrieves a note by its title, permalink, or content search,
        returning the raw markdown content including observations, relations, and metadata.
    
        Project Resolution:
        Server resolves projects in this order: Single Project Mode → project parameter → default project.
        If project unknown, use list_memory_projects() or recent_activity() first.
    
        This tool will try multiple lookup strategies to find the most relevant note:
        1. Direct permalink lookup
        2. Title search fallback
        3. Text search as last resort
    
        Args:
            project: Project name to read from. Optional - server will resolve using the
                    hierarchy above. If unknown, use list_memory_projects() to discover
                    available projects.
            identifier: The title or permalink of the note to read
                       Can be a full memory:// URL, a permalink, a title, or search text
            page: Page number for paginated results (default: 1)
            page_size: Number of items per page (default: 10)
            context: Optional FastMCP context for performance caching.
    
        Returns:
            The full markdown content of the note if found, or helpful guidance if not found.
            Content includes frontmatter, observations, relations, and all markdown formatting.
    
        Examples:
            # Read by permalink
            read_note("my-research", "specs/search-spec")
    
            # Read by title
            read_note("work-project", "Search Specification")
    
            # Read with memory URL
            read_note("my-research", "memory://specs/search-spec")
    
            # Read with pagination
            read_note("work-project", "Project Updates", page=2, page_size=5)
    
            # Read recent meeting notes
            read_note("team-docs", "Weekly Standup")
    
        Raises:
            HTTPError: If project doesn't exist or is inaccessible
            SecurityError: If identifier attempts path traversal
    
        Note:
            If the exact note isn't found, this tool provides helpful suggestions
            including related notes, search commands, and note creation templates.
        """
        track_mcp_tool("read_note")
        async with get_client() as client:
            # Get and validate the project
            active_project = await get_active_project(client, project, context)
    
            # Validate identifier to prevent path traversal attacks
            # We need to check both the raw identifier and the processed path
            processed_path = memory_url_path(identifier)
            project_path = active_project.home
    
            if not validate_project_path(identifier, project_path) or not validate_project_path(
                processed_path, project_path
            ):
                logger.warning(
                    "Attempted path traversal attack blocked",
                    identifier=identifier,
                    processed_path=processed_path,
                    project=active_project.name,
                )
                return f"# Error\n\nIdentifier '{identifier}' is not allowed - paths must stay within project boundaries"
    
            # Get the file via REST API - first try direct identifier resolution
            entity_path = memory_url_path(identifier)
            logger.info(
                f"Attempting to read note from Project: {active_project.name} identifier: {entity_path}"
            )
    
            try:
                # Try to resolve identifier to entity ID
                entity_id = await resolve_entity_id(client, active_project.external_id, entity_path)
    
                # Fetch content using entity ID
                response = await call_get(
                    client,
                    f"/v2/projects/{active_project.external_id}/resource/{entity_id}",
                    params={"page": page, "page_size": page_size},
                )
    
                # If successful, return the content
                if response.status_code == 200:
                    logger.info("Returning read_note result from resource: {path}", path=entity_path)
                    return response.text
            except Exception as e:  # pragma: no cover
                logger.info(f"Direct lookup failed for '{entity_path}': {e}")
                # Continue to fallback methods
    
            # Fallback 1: Try title search via API
            logger.info(f"Search title for: {identifier}")
            title_results = await search_notes.fn(
                query=identifier, search_type="title", project=project, context=context
            )
    
            # Handle both SearchResponse object and error strings
            if title_results and hasattr(title_results, "results") and title_results.results:
                result = title_results.results[0]  # Get the first/best match
                if result.permalink:
                    try:
                        # Resolve the permalink to entity ID
                        entity_id = await resolve_entity_id(client, active_project.external_id, result.permalink)
    
                        # Fetch content using the entity ID
                        response = await call_get(
                            client,
                            f"/v2/projects/{active_project.external_id}/resource/{entity_id}",
                            params={"page": page, "page_size": page_size},
                        )
    
                        if response.status_code == 200:
                            logger.info(f"Found note by title search: {result.permalink}")
                            return response.text
                    except Exception as e:  # pragma: no cover
                        logger.info(
                            f"Failed to fetch content for found title match {result.permalink}: {e}"
                        )
            else:
                logger.info(
                    f"No results in title search for: {identifier} in project {active_project.name}"
                )
    
            # Fallback 2: Text search as a last resort
            logger.info(f"Title search failed, trying text search for: {identifier}")
            text_results = await search_notes.fn(
                query=identifier, search_type="text", project=project, context=context
            )
    
            # We didn't find a direct match, construct a helpful error message
            # Handle both SearchResponse object and error strings
            if not text_results or not hasattr(text_results, "results") or not text_results.results:
                # No results at all
                return format_not_found_message(active_project.name, identifier)
            else:
                # We found some related results
                return format_related_results(active_project.name, identifier, text_results.results[:5])
  • Explicit import of read_note function in tools __init__.py, which triggers registration via the @mcp.tool decorator when the tools module is imported by the MCP server.
    from basic_memory.mcp.tools.read_note import read_note
  • Input schema defined by the function signature with type annotations and defaults. Returns str (markdown content or guidance). Detailed usage in docstring.
    async def read_note(
        identifier: str,
        project: Optional[str] = None,
        page: int = 1,
        page_size: int = 10,
        context: Context | None = None,
    ) -> str:
  • Helper functions used by the handler to generate user-friendly markdown responses when notes are not found or related results are available.
    def format_not_found_message(project: str | None, identifier: str) -> str:
        """Format a helpful message when no note was found."""
        return dedent(f"""
            # Note Not Found in {project}: "{identifier}"
    
            I couldn't find any notes matching "{identifier}". Here are some suggestions:
    
            ## Check Identifier Type
            - If you provided a title, try using the exact permalink instead
            - If you provided a permalink, check for typos or try a broader search
    
            ## Search Instead
            Try searching for related content:
            ```
            search_notes(project="{project}", query="{identifier}")
            ```
    
            ## Recent Activity
            Check recently modified notes:
            ```
            recent_activity(timeframe="7d")
            ```
    
            ## Create New Note
            This might be a good opportunity to create a new note on this topic:
            ```
            write_note(
                project="{project}",
                title="{identifier.capitalize()}",
                content='''
                # {identifier.capitalize()}
    
                ## Overview
                [Your content here]
    
                ## Observations
                - [category] [Observation about {identifier}]
    
                ## Relations
                - relates_to [[Related Topic]]
                ''',
                folder="notes"
            )
            ```
        """)
  • The @mcp.tool decorator that registers the read_note function with the FastMCP server instance.
    @mcp.tool(
        description="Read a markdown note by title or permalink.",
    )
Behavior2/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 states the action ('Read') but lacks details on permissions, rate limits, error handling, or output format. While it hints at retrieval by identifier, it doesn't clarify if this is a safe read operation or has side effects, leaving significant gaps.

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 a single, efficient sentence with zero waste, front-loading the core action and key parameter. It's appropriately sized for the tool's complexity, making it easy to parse without unnecessary elaboration.

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

Completeness3/5

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

Given the tool has an output schema (which handles return values), the description's gaps in parameter semantics and behavioral transparency are partially mitigated. However, with 4 parameters and no annotations, it should provide more context on usage and parameters to be fully complete, resulting in an average score.

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

Parameters2/5

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

Schema description coverage is 0%, so the description must compensate for undocumented parameters. It only mentions 'identifier' (title or permalink), ignoring 'project,' 'page,' and 'page_size.' This partial coverage fails to explain the purpose or usage of most parameters, adding minimal value beyond the schema.

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

Purpose4/5

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

The description clearly states the verb ('Read') and resource ('a markdown note'), specifying it can be accessed 'by title or permalink.' This distinguishes it from generic read operations but doesn't explicitly differentiate from sibling tools like 'view_note' or 'read_content,' keeping it from a perfect score.

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

Usage Guidelines2/5

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

No guidance is provided on when to use this tool versus alternatives such as 'view_note,' 'search_notes,' or 'read_content.' The description implies usage for retrieving notes but offers no context on prerequisites, exclusions, or comparative scenarios with siblings.

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/basicmachines-co/basic-memory'

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