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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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
Behavior2/5

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

With no annotations provided, the description carries full burden but offers limited behavioral insight. It mentions 'updating database' (implying mutation) and 'maintaining links' (hinting at referential integrity), but lacks details on permissions, error conditions, side effects, or what 'maintaining links' entails operationally. This is inadequate 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.

Conciseness5/5

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

The description is a single, efficient sentence with zero wasteβ€”it directly states the action, outcome, and key behavioral aspect ('maintaining links'). It's appropriately sized and front-loaded, making every word count 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 a mutation tool with 3 parameters (0% schema coverage), no annotations, but an output schema exists, the description is minimally complete. It covers the core action and hints at behavior, but lacks details on parameters, error handling, and operational context that would be needed for full agent understanding, though the output schema mitigates some 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 but adds minimal param semantics. It implies 'identifier' selects the note and 'destination_path' specifies the new location, but doesn't explain formats (e.g., path syntax, identifier types) or the optional 'project' parameter's role. This fails to bridge the coverage gap effectively.

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 ('Move a note') and the resource ('note'), specifying the outcome ('updating database and maintaining links'). It distinguishes from siblings like 'edit_note' or 'delete_note' by focusing on relocation, but doesn't explicitly contrast with tools like 'write_note' or 'view_note' in terms of purpose.

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?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., note existence, permissions), exclusions (e.g., when not to move), or comparisons to siblings like 'edit_note' for content changes or 'delete_note' for removal, leaving usage context unclear.

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