Skip to main content
Glama

edit_note

Modify existing markdown notes by appending, prepending, replacing text, or updating specific sections to maintain organized knowledge records.

Instructions

Edit an existing markdown note using various operations like append, prepend, find_replace, or replace_section.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
identifierYes
operationYes
contentYes
projectNo
sectionNo
find_textNo
expected_replacementsNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The primary handler function for the 'edit_note' MCP tool. Decorated with @mcp.tool for automatic schema generation and registration. Implements note editing operations (append, prepend, find_replace, replace_section) by calling the backend PATCH API, handles errors with helpful messages, and returns formatted summaries with observations and relations.
    @mcp.tool(
        description="Edit an existing markdown note using various operations like append, prepend, find_replace, or replace_section.",
    )
    async def edit_note(
        identifier: str,
        operation: str,
        content: str,
        project: Optional[str] = None,
        section: Optional[str] = None,
        find_text: Optional[str] = None,
        expected_replacements: int = 1,
        context: Context | None = None,
    ) -> str:
        """Edit an existing markdown note in the knowledge base.
    
        Makes targeted changes to existing notes without rewriting the entire content.
    
        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.
    
        Args:
            identifier: The exact title, permalink, or memory:// URL of the note to edit.
                       Must be an exact match - fuzzy matching is not supported for edit operations.
                       Use search_notes() or read_note() first to find the correct identifier if uncertain.
            operation: The editing operation to perform:
                      - "append": Add content to the end of the note
                      - "prepend": Add content to the beginning of the note
                      - "find_replace": Replace occurrences of find_text with content
                      - "replace_section": Replace content under a specific markdown header
            content: The content to add or use for replacement
            project: Project name to edit in. Optional - server will resolve using hierarchy.
                    If unknown, use list_memory_projects() to discover available projects.
            section: For replace_section operation - the markdown header to replace content under (e.g., "## Notes", "### Implementation")
            find_text: For find_replace operation - the text to find and replace
            expected_replacements: For find_replace operation - the expected number of replacements (validation will fail if actual doesn't match)
            context: Optional FastMCP context for performance caching.
    
        Returns:
            A markdown formatted summary of the edit operation and resulting semantic content,
            including operation details, file path, observations, relations, and project metadata.
    
        Examples:
            # Add new content to end of note
            edit_note("my-project", "project-planning", "append", "\\n## New Requirements\\n- Feature X\\n- Feature Y")
    
            # Add timestamp at beginning (frontmatter-aware)
            edit_note("work-docs", "meeting-notes", "prepend", "## 2025-05-25 Update\\n- Progress update...\\n\\n")
    
            # Update version number (single occurrence)
            edit_note("api-project", "config-spec", "find_replace", "v0.13.0", find_text="v0.12.0")
    
            # Update version in multiple places with validation
            edit_note("docs-project", "api-docs", "find_replace", "v2.1.0", find_text="v2.0.0", expected_replacements=3)
    
            # Replace text that appears multiple times - validate count first
            edit_note("team-docs", "docs/guide", "find_replace", "new-api", find_text="old-api", expected_replacements=5)
    
            # Replace implementation section
            edit_note("specs", "api-spec", "replace_section", "New implementation approach...\\n", section="## Implementation")
    
            # Replace subsection with more specific header
            edit_note("docs", "docs/setup", "replace_section", "Updated install steps\\n", section="### Installation")
    
            # Using different identifier formats (must be exact matches)
            edit_note("work-project", "Meeting Notes", "append", "\\n- Follow up on action items")  # exact title
            edit_note("work-project", "docs/meeting-notes", "append", "\\n- Follow up tasks")       # exact permalink
    
            # If uncertain about identifier, search first:
            # search_notes("work-project", "meeting")  # Find available notes
            # edit_note("work-project", "docs/meeting-notes-2025", "append", "content")  # Use exact result
    
            # Add new section to document
            edit_note("planning", "project-plan", "replace_section", "TBD - needs research\\n", section="## Future Work")
    
            # Update status across document (expecting exactly 2 occurrences)
            edit_note("reports", "status-report", "find_replace", "In Progress", find_text="Not Started", expected_replacements=2)
    
        Raises:
            HTTPError: If project doesn't exist or is inaccessible
            ValueError: If operation is invalid or required parameters are missing
            SecurityError: If identifier attempts path traversal
    
        Note:
            Edit operations require exact identifier matches. If unsure, use read_note() or
            search_notes() first to find the correct identifier. The tool provides detailed
            error messages with suggestions if operations fail.
        """
        track_mcp_tool("edit_note")
        async with get_client() as client:
            active_project = await get_active_project(client, project, context)
    
            logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
    
            # Validate operation
            valid_operations = ["append", "prepend", "find_replace", "replace_section"]
            if operation not in valid_operations:
                raise ValueError(
                    f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
                )
    
            # Validate required parameters for specific operations
            if operation == "find_replace" and not find_text:
                raise ValueError("find_text parameter is required for find_replace operation")
            if operation == "replace_section" and not section:
                raise ValueError("section parameter is required for replace_section operation")
    
            # Use the PATCH endpoint to edit the entity
            try:
                # Resolve identifier to entity ID
                entity_id = await resolve_entity_id(client, active_project.external_id, identifier)
    
                # Prepare the edit request data
                edit_data = {
                    "operation": operation,
                    "content": content,
                }
    
                # Add optional parameters
                if section:
                    edit_data["section"] = section
                if find_text:
                    edit_data["find_text"] = find_text
                if expected_replacements != 1:  # Only send if different from default
                    edit_data["expected_replacements"] = str(expected_replacements)
    
                # Call the PATCH endpoint
                url = f"/v2/projects/{active_project.external_id}/knowledge/entities/{entity_id}"
                response = await call_patch(client, url, json=edit_data)
                result = EntityResponse.model_validate(response.json())
    
                # Format summary
                summary = [
                    f"# Edited note ({operation})",
                    f"project: {active_project.name}",
                    f"file_path: {result.file_path}",
                    f"permalink: {result.permalink}",
                    f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
                ]
    
                # Add operation-specific details
                if operation == "append":
                    lines_added = len(content.split("\n"))
                    summary.append(f"operation: Added {lines_added} lines to end of note")
                elif operation == "prepend":
                    lines_added = len(content.split("\n"))
                    summary.append(f"operation: Added {lines_added} lines to beginning of note")
                elif operation == "find_replace":
                    # For find_replace, we can't easily count replacements from here
                    # since we don't have the original content, but the server handled it
                    summary.append("operation: Find and replace operation completed")
                elif operation == "replace_section":
                    summary.append(f"operation: Replaced content under section '{section}'")
    
                # Count observations by category (reuse logic from write_note)
                categories = {}
                if result.observations:
                    for obs in result.observations:
                        categories[obs.category] = categories.get(obs.category, 0) + 1
    
                    summary.append("\\n## Observations")
                    for category, count in sorted(categories.items()):
                        summary.append(f"- {category}: {count}")
    
                # Count resolved/unresolved relations
                unresolved = 0
                resolved = 0
                if result.relations:
                    unresolved = sum(1 for r in result.relations if not r.to_id)
                    resolved = len(result.relations) - unresolved
    
                    summary.append("\\n## Relations")
                    summary.append(f"- Resolved: {resolved}")
                    if unresolved:
                        summary.append(f"- Unresolved: {unresolved}")
    
                logger.info(
                    "MCP tool response",
                    tool="edit_note",
                    operation=operation,
                    project=active_project.name,
                    permalink=result.permalink,
                    observations_count=len(result.observations),
                    relations_count=len(result.relations),
                    status_code=response.status_code,
                )
    
                result = "\n".join(summary)
                return add_project_metadata(result, active_project.name)
    
            except Exception as e:
                logger.error(f"Error editing note: {e}")
                return _format_error_response(
                    str(e), operation, identifier, find_text, expected_replacements, active_project.name
                )
  • Import statement that loads the edit_note tool function, triggering its registration on the global MCP server instance via the @mcp.tool decorator.
    from basic_memory.mcp.tools.edit_note import edit_note
  • Helper function used by edit_note to generate user-friendly error messages with troubleshooting suggestions for various failure modes.
    def _format_error_response(
        error_message: str,
        operation: str,
        identifier: str,
        find_text: Optional[str] = None,
        expected_replacements: int = 1,
        project: Optional[str] = None,
    ) -> str:
        """Format helpful error responses for edit_note failures that guide the AI to retry successfully."""
    
        # Entity not found errors
        if "Entity not found" in error_message or "entity not found" in error_message.lower():
            return f"""# Edit Failed - Note Not Found
    
    The note with identifier '{identifier}' could not be found. Edit operations require an exact match (no fuzzy matching).
    
    ## Suggestions to try:
    1. **Search for the note first**: Use `search_notes("{project or "project-name"}", "{identifier.split("/")[-1]}")` to find similar notes with exact identifiers
    2. **Try different exact identifier formats**:
       - If you used a permalink like "folder/note-title", try the exact title: "{identifier.split("/")[-1].replace("-", " ").title()}"
       - If you used a title, try the exact permalink format: "{identifier.lower().replace(" ", "-")}"
       - Use `read_note("{project or "project-name"}", "{identifier}")` first to verify the note exists and get the exact identifier
    
    ## Alternative approach:
    Use `write_note("{project or "project-name"}", "title", "content", "folder")` to create the note first, then edit it."""
    
        # Find/replace specific errors
        if operation == "find_replace":
            if "Text to replace not found" in error_message:
                return f"""# Edit Failed - Text Not Found
    
    The text '{find_text}' was not found in the note '{identifier}'.
    
    ## Suggestions to try:
    1. **Read the note first**: Use `read_note("{project or "project-name"}", "{identifier}")` to see the current content
    2. **Check for exact matches**: The search is case-sensitive and must match exactly
    3. **Try a broader search**: Search for just part of the text you want to replace
    4. **Use expected_replacements=0**: If you want to verify the text doesn't exist
    
    ## Alternative approaches:
    - Use `append` or `prepend` to add new content instead
    - Use `replace_section` if you're trying to update a specific section"""
    
            if "Expected" in error_message and "occurrences" in error_message:
                # Extract the actual count from error message if possible
                import re
    
                match = re.search(r"found (\d+)", error_message)
                actual_count = match.group(1) if match else "a different number of"
    
                return f"""# Edit Failed - Wrong Replacement Count
    
    Expected {expected_replacements} occurrences of '{find_text}' but found {actual_count}.
    
    ## How to fix:
    1. **Read the note first**: Use `read_note("{project or "project-name"}", "{identifier}")` to see how many times '{find_text}' appears
    2. **Update expected_replacements**: Set expected_replacements={actual_count} in your edit_note call
    3. **Be more specific**: If you only want to replace some occurrences, make your find_text more specific
    
    ## Example:
    ```
    edit_note("{project or "project-name"}", "{identifier}", "find_replace", "new_text", find_text="{find_text}", expected_replacements={actual_count})
    ```"""
    
        # Section replacement errors
        if operation == "replace_section" and "Multiple sections" in error_message:
            return f"""# Edit Failed - Duplicate Section Headers
    
    Multiple sections found with the same header in note '{identifier}'.
    
    ## How to fix:
    1. **Read the note first**: Use `read_note("{project or "project-name"}", "{identifier}")` to see the document structure
    2. **Make headers unique**: Add more specific text to distinguish sections
    3. **Use append instead**: Add content at the end rather than replacing a specific section
    
    ## Alternative approach:
    Use `find_replace` to update specific text within the duplicate sections."""
    
        # Generic server/request errors
        if (
            "Invalid request" in error_message or "malformed" in error_message.lower()
        ):  # pragma: no cover
            return f"""# Edit Failed - Request Error
    
    There was a problem with the edit request to note '{identifier}': {error_message}.
    
    ## Common causes and fixes:
    1. **Note doesn't exist**: Use `search_notes("{project or "project-name"}", "query")` or `read_note("{project or "project-name"}", "{identifier}")` to verify the note exists
    2. **Invalid identifier format**: Try different identifier formats (title vs permalink)
    3. **Empty or invalid content**: Check that your content is properly formatted
    4. **Server error**: Try the operation again, or use `read_note()` first to verify the note state
    
    ## Troubleshooting steps:
    1. Verify the note exists: `read_note("{project or "project-name"}", "{identifier}")`
    2. If not found, search for it: `search_notes("{project or "project-name"}", "{identifier.split("/")[-1]}")`
    3. Try again with the correct identifier from the search results"""
    
        # Fallback for other errors
        return f"""# Edit Failed
    
    Error editing note '{identifier}': {error_message}
    
    ## General troubleshooting:
    1. **Verify the note exists**: Use `read_note("{project or "project-name"}", "{identifier}")` to check
    2. **Check your parameters**: Ensure all required parameters are provided correctly
    3. **Read the note content first**: Use `read_note("{project or "project-name"}", "{identifier}")` to understand the current structure
    4. **Try a simpler operation**: Start with `append` if other operations fail
    
    ## Need help?
    - Use `search_notes("{project or "project-name"}", "query")` to find notes
    - Use `read_note("{project or "project-name"}", "identifier")` to examine content before editing
    - Check that identifiers, section headers, and find_text match exactly"""
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It states the tool edits notes with various operations, implying mutation, but doesn't disclose behavioral traits like whether edits are reversible, permission requirements, rate limits, or error handling. This is a significant gap for a mutation tool with zero annotation coverage.

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

Conciseness4/5

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

The description is a single, efficient sentence that front-loads the core purpose ('Edit an existing markdown note') and lists operation types. It avoids redundancy and wastes no words, though it could be slightly more structured (e.g., bullet points for operations).

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 7 parameters with 0% schema coverage, no annotations, and an output schema (which reduces need to explain returns), the description is incomplete. It covers the basic purpose and operation types but misses parameter details, behavioral context, and usage guidelines. It's minimally adequate for a simple edit tool but has clear gaps.

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. It mentions operations (append, prepend, find_replace, replace_section) which partially explains the 'operation' parameter, but doesn't clarify other parameters like 'identifier', 'content', 'project', 'section', 'find_text', or 'expected_replacements'. The description adds minimal value beyond the schema's property names.

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 action ('Edit an existing markdown note') and resource ('note'), specifying the type of operations available (append, prepend, find_replace, replace_section). It distinguishes from siblings like 'write_note' (create) and 'delete_note' (remove), but doesn't explicitly differentiate from 'view_note' or 'read_note' in terms of when to edit versus read.

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 explicit guidance on when to use this tool versus alternatives is provided. It doesn't mention prerequisites (e.g., note must exist), when to choose specific operations, or when to use 'write_note' for new notes instead. The context is implied (editing existing notes), but lacks actionable usage rules.

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