Skip to main content
Glama

move_note

Relocate notes within your knowledge base while preserving database integrity and maintaining all existing links between files.

Instructions

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

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
destination_pathYes
identifierYes
projectNo

Implementation Reference

  • The primary handler for the 'move_note' MCP tool. It is decorated with @mcp.tool(), handles input validation, detects cross-project attempts, validates paths and extensions, calls the backend /knowledge/move API, and formats success/error responses with helpful guidance.
    @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 """ 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) project_url = active_project.project_url # 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: # Fetch source entity information to get the current file extension url = f"{project_url}/knowledge/entities/{identifier}" 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: # Fetch source entity information url = f"{project_url}/knowledge/entities/{identifier}" 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: # Prepare move request move_data = { "identifier": identifier, "destination_path": destination_path, "project": active_project.name, } # Call the move API endpoint url = f"{project_url}/knowledge/move" response = await call_post(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)
  • Helper function to detect if the move is attempting to target a different project and provides guidance.
    async def _detect_cross_project_move_attempt( client, identifier: str, destination_path: str, current_project: str ) -> Optional[str]: """Detect potential cross-project move attempts and return guidance. Args: client: The AsyncClient instance identifier: The note identifier being moved destination_path: The destination path current_project: The current active project Returns: Error message with guidance if cross-project move is detected, None otherwise """ try: # Get list of all available projects to check against response = await call_get(client, "/projects/projects") project_list = ProjectList.model_validate(response.json()) project_names = [p.name.lower() for p in project_list.projects] # Check if destination path contains any project names dest_lower = destination_path.lower() path_parts = dest_lower.split("/") # Look for project names in the destination path for part in path_parts: if part in project_names and part != current_project.lower(): # Found a different project name in the path matching_project = next( p.name for p in project_list.projects if p.name.lower() == part ) return _format_cross_project_error_response( identifier, destination_path, current_project, matching_project ) # No other cross-project patterns detected except Exception as e: # If we can't detect, don't interfere with normal error handling logger.debug(f"Could not check for cross-project move: {e}") return None return None
  • Helper to format user-friendly error message for cross-project move attempts, including manual workflow instructions.
    def _format_cross_project_error_response( identifier: str, destination_path: str, current_project: str, target_project: str ) -> str: """Format error response for detected cross-project move attempts.""" return dedent(f""" # Move Failed - Cross-Project Move Not Supported Cannot move '{identifier}' to '{destination_path}' because it appears to reference a different project ('{target_project}'). **Current project:** {current_project} **Target project:** {target_project} ## Cross-project moves are not supported directly Notes can only be moved within the same project. To move content between projects, use this workflow: ### Recommended approach: ``` # 1. Read the note content from current project read_note("{identifier}") # 2. Create the note in the target project write_note("Note Title", "content from step 1", "target-folder", project="{target_project}") # 3. Delete the original note if desired delete_note("{identifier}", project="{current_project}") ``` ### Alternative: Stay in current project If you want to move the note within the **{current_project}** project only: ``` move_note("{identifier}", "new-folder/new-name.md") ``` ## Available projects: Use `list_memory_projects()` to see all available projects. """).strip()

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