Skip to main content
Glama

move_note

Relocate notes within the knowledge graph while preserving database integrity and maintaining all existing connections between files.

Instructions

Move a note to a new location, updating database and maintaining links.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
identifierYes
destination_pathYes
projectNo

Implementation Reference

  • The primary handler for the 'move_note' MCP tool. Decorated with @mcp.tool(), it resolves the note identifier to an entity ID, validates the destination path, detects potential cross-project moves, performs the move via the v2 API endpoint (/knowledge/entities/{entity_id}/move), handles errors with user-friendly formatted responses, and returns success details including updated permalink.
    @mcp.tool(
        description="Move a note to a new location, updating database and maintaining links.",
    )
    async def move_note(
        identifier: str,
        destination_path: str,
        project: Optional[str] = None,
        context: Context | None = None,
    ) -> str:
        """Move a note to a new file location within the same project.
    
        Moves a note from one location to another within the project, updating all
        database references and maintaining semantic content. Uses stateless architecture -
        project parameter optional with server resolution.
    
        Args:
            identifier: Exact entity identifier (title, permalink, or memory:// URL).
                       Must be an exact match - fuzzy matching is not supported for move operations.
                       Use search_notes() or read_note() first to find the correct identifier if uncertain.
            destination_path: New path relative to project root (e.g., "work/meetings/2025-05-26.md")
            project: Project name to move within. Optional - server will resolve using hierarchy.
                    If unknown, use list_memory_projects() to discover available projects.
            context: Optional FastMCP context for performance caching.
    
        Returns:
            Success message with move details and project information.
    
        Examples:
            # Move to new folder (exact title match)
            move_note("My Note", "work/notes/my-note.md")
    
            # Move by exact permalink
            move_note("my-note-permalink", "archive/old-notes/my-note.md")
    
            # Move with complex path structure
            move_note("experiments/ml-results", "archive/2025/ml-experiments.md")
    
            # Explicit project specification
            move_note("My Note", "work/notes/my-note.md", project="work-project")
    
            # If uncertain about identifier, search first:
            # search_notes("my note")  # Find available notes
            # move_note("docs/my-note-2025", "archive/my-note.md")  # Use exact result
    
        Raises:
            ToolError: If project doesn't exist, identifier is not found, or destination_path is invalid
    
        Note:
            This operation moves notes within the specified project only. Moving notes
            between different projects is not currently supported.
    
        The move operation:
        - Updates the entity's file_path in the database
        - Moves the physical file on the filesystem
        - Optionally updates permalinks if configured
        - Re-indexes the entity for search
        - Maintains all observations and relations
        """
        track_mcp_tool("move_note")
        async with get_client() as client:
            logger.debug(f"Moving note: {identifier} to {destination_path} in project: {project}")
    
            active_project = await get_active_project(client, project, context)
    
            # Validate destination path to prevent path traversal attacks
            project_path = active_project.home
            if not validate_project_path(destination_path, project_path):
                logger.warning(
                    "Attempted path traversal attack blocked",
                    destination_path=destination_path,
                    project=active_project.name,
                )
                return f"""# Move Failed - Security Validation Error
    
    The destination path '{destination_path}' is not allowed - paths must stay within project boundaries.
    
    ## Valid path examples:
    - `notes/my-file.md`
    - `projects/2025/meeting-notes.md`
    - `archive/old-notes.md`
    
    ## Try again with a safe path:
    ```
    move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
    ```"""
    
            # Check for potential cross-project move attempts
            cross_project_error = await _detect_cross_project_move_attempt(
                client, identifier, destination_path, active_project.name
            )
            if cross_project_error:
                logger.info(f"Detected cross-project move attempt: {identifier} -> {destination_path}")
                return cross_project_error
    
            # Get the source entity information for extension validation
            source_ext = "md"  # Default to .md if we can't determine source extension
            try:
                # Resolve identifier to entity ID
                entity_id = await resolve_entity_id(client, active_project.external_id, identifier)
                # Fetch source entity information to get the current file extension
                url = f"/v2/projects/{active_project.external_id}/knowledge/entities/{entity_id}"
                response = await call_get(client, url)
                source_entity = EntityResponse.model_validate(response.json())
                if "." in source_entity.file_path:
                    source_ext = source_entity.file_path.split(".")[-1]
            except Exception as e:
                # If we can't fetch the source entity, default to .md extension
                logger.debug(f"Could not fetch source entity for extension check: {e}")
    
            # Validate that destination path includes a file extension
            if "." not in destination_path or not destination_path.split(".")[-1]:
                logger.warning(f"Move failed - no file extension provided: {destination_path}")
                return dedent(f"""
                    # Move Failed - File Extension Required
    
                    The destination path '{destination_path}' must include a file extension (e.g., '.md').
    
                    ## Valid examples:
                    - `notes/my-note.md`
                    - `projects/meeting-2025.txt`
                    - `archive/old-program.sh`
    
                    ## Try again with extension:
                    ```
                    move_note("{identifier}", "{destination_path}.{source_ext}")
                    ```
    
                    All examples in Basic Memory expect file extensions to be explicitly provided.
                    """).strip()
    
            # Get the source entity to check its file extension
            try:
                # Resolve identifier to entity ID (might already be cached from above)
                entity_id = await resolve_entity_id(client, active_project.external_id, identifier)
                # Fetch source entity information
                url = f"/v2/projects/{active_project.external_id}/knowledge/entities/{entity_id}"
                response = await call_get(client, url)
                source_entity = EntityResponse.model_validate(response.json())
    
                # Extract file extensions
                source_ext = (
                    source_entity.file_path.split(".")[-1] if "." in source_entity.file_path else ""
                )
                dest_ext = destination_path.split(".")[-1] if "." in destination_path else ""
    
                # Check if extensions match
                if source_ext and dest_ext and source_ext.lower() != dest_ext.lower():
                    logger.warning(
                        f"Move failed - file extension mismatch: source={source_ext}, dest={dest_ext}"
                    )
                    return dedent(f"""
                        # Move Failed - File Extension Mismatch
    
                        The destination file extension '.{dest_ext}' does not match the source file extension '.{source_ext}'.
    
                        To preserve file type consistency, the destination must have the same extension as the source.
    
                        ## Source file:
                        - Path: `{source_entity.file_path}`
                        - Extension: `.{source_ext}`
    
                        ## Try again with matching extension:
                        ```
                        move_note("{identifier}", "{destination_path.rsplit(".", 1)[0]}.{source_ext}")
                        ```
                        """).strip()
            except Exception as e:
                # If we can't fetch the source entity, log it but continue
                # This might happen if the identifier is not yet resolved
                logger.debug(f"Could not fetch source entity for extension check: {e}")
    
            try:
                # Resolve identifier to entity ID for the move operation
                entity_id = await resolve_entity_id(client, active_project.external_id, identifier)
    
                # Prepare move request (v2 API only needs destination_path)
                move_data = {
                    "destination_path": destination_path,
                }
    
                # Call the v2 move API endpoint (PUT method, entity_id in URL)
                url = f"/v2/projects/{active_project.external_id}/knowledge/entities/{entity_id}/move"
                response = await call_put(client, url, json=move_data)
                result = EntityResponse.model_validate(response.json())
    
                # Build success message
                result_lines = [
                    "βœ… Note moved successfully",
                    "",
                    f"πŸ“ **{identifier}** β†’ **{result.file_path}**",
                    f"πŸ”— Permalink: {result.permalink}",
                    "πŸ“Š Database and search index updated",
                    "",
                    f"<!-- Project: {active_project.name} -->",
                ]
    
                # Log the operation
                logger.info(
                    "Move note completed",
                    identifier=identifier,
                    destination_path=destination_path,
                    project=active_project.name,
                    status_code=response.status_code,
                )
    
                return "\n".join(result_lines)
    
            except Exception as e:
                logger.error(f"Move failed for '{identifier}' to '{destination_path}': {e}")
                # Return formatted error message for better user experience
                return _format_move_error_response(str(e), identifier, destination_path)
  • Explicit import of the move_note function from its module, which executes the @mcp.tool() decorator to register the tool with the FastMCP server instance.
    from basic_memory.mcp.tools.move_note import move_note

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