Skip to main content
Glama
jordanburke

joplin-mcp-server

edit_folder

Modify an existing Joplin folder by updating its title or parent folder using the folder ID. Simplify folder management and organization.

Instructions

Edit/update an existing folder/notebook in Joplin

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
folder_idYesID of the folder to edit
parent_idNoNew parent folder ID
titleNoNew folder title

Implementation Reference

  • Core handler logic for the 'edit_folder' tool. Validates input, interacts with Joplin API to update folder, handles errors, and formats success response with before/after details.
    async call(options: EditFolderOptions): Promise<string> {
      if (!options || typeof options !== "object") {
        return 'Please provide folder edit options. Example: edit_folder {"folder_id": "abc123", "title": "New Name"}'
      }
    
      // Validate required folder_id
      if (!options.folder_id) {
        return 'Please provide folder edit options. Example: edit_folder {"folder_id": "abc123", "title": "New Name"}'
      }
    
      const folderIdError = this.validateId(options.folder_id, "notebook")
      if (folderIdError) {
        return folderIdError.replace("notebook ID", "folder ID").replace("notebook_id", "folder_id")
      }
    
      // Validate that we have at least one field to update
      const updateFields = ["title", "parent_id"]
      const hasUpdate = updateFields.some((field) => options[field as keyof EditFolderOptions] !== undefined)
    
      if (!hasUpdate) {
        return "Please provide at least one field to update. Available fields: title, parent_id"
      }
    
      // Validate title if provided
      if (options.title !== undefined && (typeof options.title !== "string" || options.title.trim() === "")) {
        return "Title must be a non-empty string."
      }
    
      // Validate parent_id if provided
      if (options.parent_id !== undefined && options.parent_id !== null && options.parent_id !== "") {
        if (options.parent_id.length < 10 || !options.parent_id.match(/[a-f0-9]/i)) {
          return `Error: "${options.parent_id}" does not appear to be a valid parent notebook ID.\n\nNotebook IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse list_notebooks to see available notebooks and their IDs.`
        }
    
        // Prevent self-parenting
        if (options.parent_id === options.folder_id) {
          return "Error: A folder cannot be its own parent."
        }
      }
    
      try {
        // First, get the current folder to show before/after comparison
        const currentFolder = await this.apiClient.get<JoplinFolder>(`/folders/${options.folder_id}`, {
          query: { fields: "id,title,parent_id" },
        })
    
        if (!currentFolder || !currentFolder.id) {
          return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`
        }
    
        // Prepare the update body - only include fields that are being updated
        const updateBody: Partial<EditFolderOptions> = {}
    
        if (options.title !== undefined) updateBody.title = options.title.trim()
        if (options.parent_id !== undefined) updateBody.parent_id = options.parent_id
    
        // Update the folder
        const updatedFolder = await this.apiClient.put<EditFolderResponse>(`/folders/${options.folder_id}`, updateBody)
    
        // Validate response
        if (!updatedFolder || typeof updatedFolder !== "object" || !updatedFolder.id) {
          return "Error: Unexpected response format from Joplin API when updating folder"
        }
    
        // Get parent folder info for both old and new locations if parent_id changed
        let oldParentInfo = "Top level"
        let newParentInfo = "Top level"
    
        if (currentFolder.parent_id) {
          try {
            const oldParent = await this.apiClient.get(`/folders/${currentFolder.parent_id}`, {
              query: { fields: "title" },
            })
            if (oldParent?.title) {
              oldParentInfo = `Inside "${oldParent.title}"`
            }
          } catch {
            oldParentInfo = `Parent ID: ${currentFolder.parent_id}`
          }
        }
    
        if (updatedFolder.parent_id && updatedFolder.parent_id !== currentFolder.parent_id) {
          try {
            const newParent = await this.apiClient.get(`/folders/${updatedFolder.parent_id}`, {
              query: { fields: "title" },
            })
            if (newParent?.title) {
              newParentInfo = `Inside "${newParent.title}"`
            }
          } catch {
            newParentInfo = `Parent ID: ${updatedFolder.parent_id}`
          }
        } else if (updatedFolder.parent_id) {
          newParentInfo = oldParentInfo
        }
    
        // Format success response with before/after comparison
        const resultLines: string[] = []
        resultLines.push(`βœ… Successfully updated notebook!`)
        resultLines.push("")
        resultLines.push(`πŸ“ Notebook: "${updatedFolder.title}"`)
        resultLines.push(`   Folder ID: ${updatedFolder.id}`)
        resultLines.push("")
    
        // Show what changed
        resultLines.push(`πŸ”„ Changes made:`)
    
        if (options.title !== undefined && currentFolder.title !== updatedFolder.title) {
          resultLines.push(`   Title: "${currentFolder.title}" β†’ "${updatedFolder.title}"`)
        }
    
        if (options.parent_id !== undefined && currentFolder.parent_id !== updatedFolder.parent_id) {
          resultLines.push(`   Location: ${oldParentInfo} β†’ ${newParentInfo}`)
        }
    
        if (updatedFolder.updated_time) {
          const updatedTime = this.formatDate(updatedFolder.updated_time)
          resultLines.push(`   Last Updated: ${updatedTime}`)
        }
    
        resultLines.push("")
        resultLines.push(`πŸ”— Next steps:`)
        resultLines.push(`   - View notebook: read_notebook notebook_id="${updatedFolder.id}"`)
        resultLines.push(`   - View all notebooks: list_notebooks`)
        if (updatedFolder.parent_id) {
          resultLines.push(`   - View parent notebook: read_notebook notebook_id="${updatedFolder.parent_id}"`)
        }
    
        return resultLines.join("\n")
      } catch (error: any) {
        if (error.response) {
          if (error.response.status === 404) {
            if (error.config?.url?.includes(`/folders/${options.folder_id}`)) {
              return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`
            }
            if (options.parent_id) {
              return `Error: Parent folder with ID "${options.parent_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`
            }
          }
          if (error.response.status === 400) {
            return `Error updating folder: Invalid request data.\n\nPlease check your input parameters. ${error.response.data?.error || ""}`
          }
          if (error.response.status === 409) {
            return `Error: A folder with the title "${options.title}" might already exist in this location.\n\nTry a different title or check existing folders with list_notebooks.`
          }
        }
        return this.formatError(error, "updating folder")
      }
    }
  • TypeScript interfaces defining the input parameters (EditFolderOptions) and response type for the edit_folder tool.
    interface EditFolderOptions {
      folder_id: string
      title?: string | undefined
      parent_id?: string | undefined
    }
    
    interface EditFolderResponse extends JoplinFolder {
      updated_time: number
    }
  • Registration of the 'edit_folder' tool in the FastMCP server, including Zod schema for parameters and execution delegating to manager.editFolder.
    // Add edit_folder tool
    server.addTool({
      name: "edit_folder",
      description: "Edit/update an existing folder/notebook in Joplin",
      parameters: z.object({
        folder_id: z.string().describe("ID of the folder to edit"),
        title: z.string().optional().describe("New folder title"),
        parent_id: z.string().optional().describe("New parent folder ID"),
      }),
      execute: async (args) => {
        return await manager.editFolder(args)
      },
    })
  • src/index.ts:290-298 (registration)
    Dispatch/registration handler in stdio MCP server for 'edit_folder' tool calls, delegating to manager.editFolder.
    case "edit_folder": {
      const editFolderResult = await manager.editFolder(
        args as {
          folder_id: string
          title?: string | undefined
          parent_id?: string | undefined
        },
      )
      return { content: [{ type: "text", text: editFolderResult }], isError: false }
  • Wrapper method in JoplinServerManager that instantiates and calls the EditFolder tool instance.
    async editFolder(params: {
      folder_id: string
      title?: string | undefined
      parent_id?: string | undefined
    }): Promise<string> {
      return await this.tools.editFolder.call(params)
    }
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