delete_folder
Remove a folder or notebook from Joplin using its unique ID. Requires confirmation and supports force deletion for folders with contents.
Instructions
Delete a folder/notebook from Joplin (requires confirmation)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| confirm | No | Confirmation flag | |
| folder_id | Yes | ID of the folder to delete | |
| force | No | Force delete even if folder has contents |
Implementation Reference
- src/lib/tools/delete-folder.ts:13-165 (handler)The DeleteFolder class extending BaseTool contains the core execution logic for the delete_folder tool. It handles input validation, confirmation prompts, checks folder contents, warns about non-empty folders, performs safety checks, and executes the Joplin API deletion (/folders/{id}). Includes force delete option and detailed success/error responses.class DeleteFolder extends BaseTool { async call(options: DeleteFolderOptions): Promise<string> { if (!options || typeof options !== "object") { return 'Please provide folder deletion options. Example: delete_folder {"folder_id": "abc123", "confirm": true}' } // Validate required folder_id if (!options.folder_id) { return 'Please provide folder deletion options. Example: delete_folder {"folder_id": "abc123", "confirm": true}' } const folderIdError = this.validateId(options.folder_id, "notebook") if (folderIdError) { return folderIdError.replace("notebook ID", "folder ID").replace("notebook_id", "folder_id") } // Require explicit confirmation for safety if (!options.confirm) { return `⚠️ This will permanently delete the notebook/folder!\n\nTo confirm deletion, use:\ndelete_folder {"folder_id": "${options.folder_id}", "confirm": true}\n\n⚠️ This action cannot be undone!` } try { // First, get the folder details to show what's being deleted const folderToDelete = await this.apiClient.get<JoplinFolder>(`/folders/${options.folder_id}`, { query: { fields: "id,title,parent_id" }, }) if (!folderToDelete || !folderToDelete.id) { return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.` } // Check if folder contains notes or subfolders const [notes, subfolders] = await Promise.all([ this.apiClient .get<FolderContents>(`/folders/${options.folder_id}/notes`, { query: { fields: "id,title" }, }) .catch(() => ({ items: [] })), this.apiClient .get<FolderContents>("/folders", { query: { fields: "id,title,parent_id" }, }) .then((response) => ({ items: response.items?.filter((folder: any) => folder.parent_id === options.folder_id) || [], })) .catch(() => ({ items: [] })), ]) const noteCount = notes.items?.length || 0 const subfolderCount = subfolders.items?.length || 0 const totalContent = noteCount + subfolderCount // Warn if folder is not empty and force is not specified if (totalContent > 0 && !options.force) { const resultLines: string[] = [] resultLines.push(`⚠️ Cannot delete non-empty notebook!`) resultLines.push("") resultLines.push(`📁 Notebook: "${folderToDelete.title}"`) resultLines.push(` Contains: ${noteCount} notes and ${subfolderCount} subfolders`) if (noteCount > 0) { resultLines.push("") resultLines.push(`📝 Contains ${noteCount} notes:`) notes.items.slice(0, 5).forEach((note: any) => { resultLines.push(` - ${note.title || "Untitled"}`) }) if (noteCount > 5) { resultLines.push(` ... and ${noteCount - 5} more notes`) } } if (subfolderCount > 0) { resultLines.push("") resultLines.push(`📁 Contains ${subfolderCount} subfolders:`) subfolders.items.slice(0, 5).forEach((folder: any) => { resultLines.push(` - ${folder.title}`) }) if (subfolderCount > 5) { resultLines.push(` ... and ${subfolderCount - 5} more folders`) } } resultLines.push("") resultLines.push(`💡 Options:`) resultLines.push(` 1. Move or delete the contents first, then delete the folder`) resultLines.push(` 2. Force delete (⚠️ DESTROYS ALL CONTENT):`) resultLines.push(` delete_folder {"folder_id": "${options.folder_id}", "confirm": true, "force": true}`) resultLines.push("") resultLines.push(`⚠️ Force delete will permanently delete ALL ${totalContent} items inside!`) return resultLines.join("\n") } // Get parent folder info if available let parentInfo = "Top level" if (folderToDelete.parent_id) { try { const parentFolder = await this.apiClient.get(`/folders/${folderToDelete.parent_id}`, { query: { fields: "title" }, }) if (parentFolder?.title) { parentInfo = `Inside "${parentFolder.title}" (notebook_id: "${folderToDelete.parent_id}")` } } catch { parentInfo = `Parent ID: ${folderToDelete.parent_id}` } } // Delete the folder await this.apiClient.delete(`/folders/${options.folder_id}`) // Format success response const resultLines: string[] = [] resultLines.push(`🗑️ Successfully deleted notebook!`) resultLines.push("") resultLines.push(`📁 Deleted Notebook Details:`) resultLines.push(` Title: "${folderToDelete.title}"`) resultLines.push(` Folder ID: ${folderToDelete.id}`) resultLines.push(` Location: ${parentInfo}`) if (totalContent > 0) { resultLines.push(` Deleted Content: ${noteCount} notes and ${subfolderCount} subfolders`) resultLines.push("") resultLines.push(`⚠️ All ${totalContent} items inside have been permanently deleted!`) } resultLines.push("") resultLines.push(`⚠️ This notebook has been permanently deleted and cannot be recovered.`) if (folderToDelete.parent_id) { resultLines.push("") resultLines.push(`🔗 Related actions:`) resultLines.push(` - View parent notebook: read_notebook notebook_id="${folderToDelete.parent_id}"`) resultLines.push(` - View all notebooks: list_notebooks`) } return resultLines.join("\n") } catch (error: any) { if (error.response) { if (error.response.status === 404) { return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.` } if (error.response.status === 403) { return `Permission denied: Cannot delete folder with ID "${options.folder_id}".\n\nThis might be a protected system folder.` } if (error.response.status === 409) { return `Cannot delete folder: It may contain items that prevent deletion.\n\nTry moving or deleting the contents first, or use force option.` } } return this.formatError(error, "deleting folder") } } }
- src/lib/tools/delete-folder.ts:3-7 (schema)Interface defining the input schema for the delete_folder tool: required folder_id, optional confirm and force flags.interface DeleteFolderOptions { folder_id: string confirm?: boolean | undefined force?: boolean | undefined }
- src/index.ts:202-213 (registration)Tool registration in stdio MCP server: schema definition in listTools response.name: "delete_folder", description: "Delete a folder/notebook from Joplin (requires confirmation)", inputSchema: { type: "object", properties: { folder_id: { type: "string", description: "ID of the folder to delete" }, confirm: { type: "boolean", description: "Confirmation flag" }, force: { type: "boolean", description: "Force delete even if folder has contents" }, }, required: ["folder_id"], }, },
- src/index.ts:311-319 (registration)Dispatch handler in stdio MCP server callToolRequest: routes delete_folder calls to manager.deleteFolder.case "delete_folder": { const deleteFolderResult = await manager.deleteFolder( args as { folder_id: string confirm?: boolean | undefined force?: boolean | undefined }, ) return { content: [{ type: "text", text: deleteFolderResult }], isError: false }
- src/server-fastmcp.ts:180-191 (registration)Tool registration in FastMCP HTTP server using server.addTool with Zod schema and execute handler.server.addTool({ name: "delete_folder", description: "Delete a folder/notebook from Joplin (requires confirmation)", parameters: z.object({ folder_id: z.string().describe("ID of the folder to delete"), confirm: z.boolean().optional().describe("Confirmation flag"), force: z.boolean().optional().describe("Force delete even if folder has contents"), }), execute: async (args) => { return await manager.deleteFolder(args) }, })
- src/server-core.ts:57-57 (registration)Instantiation of DeleteFolder tool instance in JoplinServerManager for use by both stdio and HTTP servers.deleteFolder: new DeleteFolder(this.apiClient),