Skip to main content
Glama
jordanburke

joplin-mcp-server

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
NameRequiredDescriptionDefault
confirmNoConfirmation flag
folder_idYesID of the folder to delete
forceNoForce delete even if folder has contents

Implementation Reference

  • 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")
        }
      }
    }
  • 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 }
  • 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)
      },
    })
  • Instantiation of DeleteFolder tool instance in JoplinServerManager for use by both stdio and HTTP servers.
    deleteFolder: new DeleteFolder(this.apiClient),
Behavior3/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It adds useful context about the confirmation requirement, but it doesn't cover other critical aspects like whether deletion is permanent, what happens to nested content, or error conditions. This leaves gaps in understanding the tool's behavior.

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 that is front-loaded with the core action and key constraint ('requires confirmation'). There is no wasted text, making it highly concise and well-structured for quick understanding.

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 the complexity of a destructive operation with no annotations and no output schema, the description is incomplete. It mentions confirmation but lacks details on outcomes, error handling, or dependencies. For a deletion tool, this leaves significant gaps in contextual understanding that could hinder effective use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters (confirm, folder_id, force). The description doesn't add any additional meaning or syntax details beyond what the schema provides, such as how confirmation works or force implications. Baseline 3 is appropriate when schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Delete') and the resource ('a folder/notebook from Joplin'), making the purpose specific and unambiguous. It distinguishes from siblings like 'delete_note' by specifying folder-level deletion, though it doesn't explicitly mention sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context by stating 'requires confirmation,' but it doesn't provide explicit guidance on when to use this tool versus alternatives like 'delete_note' or 'edit_folder.' No exclusions or prerequisites are mentioned, leaving usage somewhat vague.

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

Related 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/jordanburke/joplin-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server