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