From c3adf20ffd729f13724ffd5640d6f445f805540a Mon Sep 17 00:00:00 2001 From: Esteban-Rod Date: Wed, 29 Apr 2026 13:32:49 -0500 Subject: [PATCH 1/4] feat(update_space): expose space update with ClickApps toggling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the update_space tool that wraps PUT /space/{id} with named flags for the most common ClickApps (priorities, milestones, time_tracking, tags, checklists, custom_fields, sprints, points, etc.) plus 'private' and 'multiple_assignees'. GOTCHA documented in tool description: ClickUp's PUT /space/{id} NULLifies fields that are absent from the body, including 'name'. The 'name' parameter is therefore REQUIRED — pass the current name even when only toggling features. The wrapper tool surfaces this in its schema description so callers don't get bitten. --- src/clickup-client/spaces.ts | 36 ++++++++++++++++++++++ src/tools/space-tools.ts | 58 +++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/clickup-client/spaces.ts b/src/clickup-client/spaces.ts index 1428ed8..7897140 100644 --- a/src/clickup-client/spaces.ts +++ b/src/clickup-client/spaces.ts @@ -69,6 +69,42 @@ export class SpacesClient { const response = await this.client.get(`/space/${spaceId}`); return response; } + + /** + * Update an existing space (name, multiple_assignees, features/ClickApps). + * GOTCHA: ClickUp's PUT /space/{id} endpoint NULLifies fields that are + * absent from the body. Always include `name` when updating other fields, + * otherwise the space will be renamed to null. + * @param spaceId The ID of the space to update + * @param params Update payload — must include name to avoid NULLification + * @returns The updated space + */ + async updateSpace(spaceId: string, params: UpdateSpaceParams): Promise { + return this.client.put(`/space/${spaceId}`, params); + } +} + +export interface UpdateSpaceParams { + name: string; // required to avoid NULLification — see updateSpace doc + private?: boolean; + multiple_assignees?: boolean; + features?: { + due_dates?: { enabled?: boolean; start_date?: boolean; remap_due_dates?: boolean; remap_closed_due_date?: boolean }; + time_tracking?: { enabled?: boolean }; + tags?: { enabled?: boolean }; + time_estimates?: { enabled?: boolean }; + checklists?: { enabled?: boolean }; + custom_fields?: { enabled?: boolean }; + remap_dependencies?: { enabled?: boolean }; + dependency_warning?: { enabled?: boolean }; + portfolios?: { enabled?: boolean }; + sprints?: { enabled?: boolean }; + points?: { enabled?: boolean }; + custom_items?: { enabled?: boolean }; + priorities?: { enabled?: boolean; priorities?: any[] }; + milestones?: { enabled?: boolean }; + zoom?: { enabled?: boolean }; + }; } export const createSpacesClient = (client: ClickUpClient): SpacesClient => { diff --git a/src/tools/space-tools.ts b/src/tools/space-tools.ts index 234700c..005bda4 100644 --- a/src/tools/space-tools.ts +++ b/src/tools/space-tools.ts @@ -42,7 +42,7 @@ export function setupSpaceTools(server: McpServer): void { console.error(`[SpaceTools] Getting space ${space_id}...`); const space = await spacesClient.getSpace(space_id); console.error(`[SpaceTools] Got space: ${space.name}`); - + return { content: [{ type: 'text', text: JSON.stringify(space, null, 2) }] }; @@ -55,4 +55,60 @@ export function setupSpaceTools(server: McpServer): void { } } ); + + // Register update_space tool + server.tool( + 'update_space', + 'Update a ClickUp space — rename, toggle multiple_assignees, or enable/disable ClickApps (priorities, milestones, time_tracking, tags, etc.). IMPORTANT: ClickUp\'s PUT /space/{id} NULLifies missing fields. The "name" parameter is required to avoid NULLification of the space name even when only toggling features.', + { + space_id: z.string().describe('The ID of the space to update'), + name: z.string().describe('Space name (REQUIRED — pass the current name to avoid NULLification when toggling features)'), + private: z.boolean().optional().describe('Whether the space is private'), + multiple_assignees: z.boolean().optional().describe('Enable assigning multiple users to a single task'), + enable_priorities: z.boolean().optional().describe('Enable the priorities ClickApp'), + enable_milestones: z.boolean().optional().describe('Enable the milestones ClickApp (custom_item_id=1 task type)'), + enable_time_tracking: z.boolean().optional().describe('Enable native time tracking'), + enable_time_estimates: z.boolean().optional().describe('Enable time estimates'), + enable_tags: z.boolean().optional().describe('Enable tags'), + enable_checklists: z.boolean().optional().describe('Enable checklists'), + enable_custom_fields: z.boolean().optional().describe('Enable custom fields'), + enable_remap_dependencies: z.boolean().optional().describe('Enable remap dependencies'), + enable_dependency_warning: z.boolean().optional().describe('Enable dependency warnings'), + enable_portfolios: z.boolean().optional().describe('Enable portfolios'), + enable_sprints: z.boolean().optional().describe('Enable sprints'), + enable_points: z.boolean().optional().describe('Enable story points') + }, + async (args) => { + try { + const { space_id, name, ...flags } = args; + const features: any = {}; + if (flags.enable_priorities !== undefined) features.priorities = { enabled: flags.enable_priorities }; + if (flags.enable_milestones !== undefined) features.milestones = { enabled: flags.enable_milestones }; + if (flags.enable_time_tracking !== undefined) features.time_tracking = { enabled: flags.enable_time_tracking }; + if (flags.enable_time_estimates !== undefined) features.time_estimates = { enabled: flags.enable_time_estimates }; + if (flags.enable_tags !== undefined) features.tags = { enabled: flags.enable_tags }; + if (flags.enable_checklists !== undefined) features.checklists = { enabled: flags.enable_checklists }; + if (flags.enable_custom_fields !== undefined) features.custom_fields = { enabled: flags.enable_custom_fields }; + if (flags.enable_remap_dependencies !== undefined) features.remap_dependencies = { enabled: flags.enable_remap_dependencies }; + if (flags.enable_dependency_warning !== undefined) features.dependency_warning = { enabled: flags.enable_dependency_warning }; + if (flags.enable_portfolios !== undefined) features.portfolios = { enabled: flags.enable_portfolios }; + if (flags.enable_sprints !== undefined) features.sprints = { enabled: flags.enable_sprints }; + if (flags.enable_points !== undefined) features.points = { enabled: flags.enable_points }; + const params: any = { name }; + if (flags.private !== undefined) params.private = flags.private; + if (flags.multiple_assignees !== undefined) params.multiple_assignees = flags.multiple_assignees; + if (Object.keys(features).length > 0) params.features = features; + const result = await spacesClient.updateSpace(space_id, params); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error updating space:', error); + return { + content: [{ type: 'text', text: `Error updating space: ${error.message}` }], + isError: true + }; + } + } + ); } From b6595272816a723b79b653212ceb9437a5df122e Mon Sep 17 00:00:00 2001 From: Esteban-Rod Date: Wed, 29 Apr 2026 14:55:37 -0500 Subject: [PATCH 2/4] feat: add create_space and delete_space tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new tools to complete space-level CRUD coverage in the fork: - create_space: POST /team/{id}/space with optional ClickApps toggles (priorities, milestones, time_tracking, tags, etc.) and multiple_assignees flag at creation time - delete_space: DELETE /space/{id} (irreversible — caller must confirm before invoking, the destructive nature is documented in the tool description) CreateSpaceParams reuses UpdateSpaceParams['features'] shape so any ClickApp toggle available on update is also available on create. --- src/clickup-client/spaces.ts | 24 ++++++++++++ src/tools/space-tools.ts | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/src/clickup-client/spaces.ts b/src/clickup-client/spaces.ts index 7897140..aa8028f 100644 --- a/src/clickup-client/spaces.ts +++ b/src/clickup-client/spaces.ts @@ -82,6 +82,30 @@ export class SpacesClient { async updateSpace(spaceId: string, params: UpdateSpaceParams): Promise { return this.client.put(`/space/${spaceId}`, params); } + + /** + * Create a new space in a workspace. + * @param workspaceId The ID of the workspace (team) + * @param params Create payload — name is required + * @returns The created space + */ + async createSpace(workspaceId: string, params: CreateSpaceParams): Promise { + return this.client.post(`/team/${workspaceId}/space`, params); + } + + /** + * Delete a space. + * @param spaceId The ID of the space to delete + */ + async deleteSpace(spaceId: string): Promise { + await this.client.delete(`/space/${spaceId}`); + } +} + +export interface CreateSpaceParams { + name: string; + multiple_assignees?: boolean; + features?: UpdateSpaceParams['features']; } export interface UpdateSpaceParams { diff --git a/src/tools/space-tools.ts b/src/tools/space-tools.ts index 005bda4..d6d6e38 100644 --- a/src/tools/space-tools.ts +++ b/src/tools/space-tools.ts @@ -111,4 +111,79 @@ export function setupSpaceTools(server: McpServer): void { } } ); + + // Register create_space tool + server.tool( + 'create_space', + 'Create a new space in a ClickUp workspace. Optionally enable ClickApps (priorities, milestones, time_tracking, tags, etc.) and toggle multiple_assignees at creation time. Use update_space afterwards to adjust further settings.', + { + workspace_id: z.string().describe('The ID of the workspace (team) to create the space in'), + name: z.string().describe('Space name'), + multiple_assignees: z.boolean().optional().describe('Enable assigning multiple users to a single task'), + enable_priorities: z.boolean().optional().describe('Enable the priorities ClickApp'), + enable_milestones: z.boolean().optional().describe('Enable the milestones ClickApp (custom_item_id=1 task type)'), + enable_time_tracking: z.boolean().optional().describe('Enable native time tracking'), + enable_time_estimates: z.boolean().optional().describe('Enable time estimates'), + enable_tags: z.boolean().optional().describe('Enable tags'), + enable_checklists: z.boolean().optional().describe('Enable checklists'), + enable_custom_fields: z.boolean().optional().describe('Enable custom fields'), + enable_remap_dependencies: z.boolean().optional().describe('Enable remap dependencies'), + enable_dependency_warning: z.boolean().optional().describe('Enable dependency warnings'), + enable_portfolios: z.boolean().optional().describe('Enable portfolios'), + enable_sprints: z.boolean().optional().describe('Enable sprints'), + enable_points: z.boolean().optional().describe('Enable story points') + }, + async (args) => { + try { + const { workspace_id, name, ...flags } = args; + const features: any = {}; + if (flags.enable_priorities !== undefined) features.priorities = { enabled: flags.enable_priorities }; + if (flags.enable_milestones !== undefined) features.milestones = { enabled: flags.enable_milestones }; + if (flags.enable_time_tracking !== undefined) features.time_tracking = { enabled: flags.enable_time_tracking }; + if (flags.enable_time_estimates !== undefined) features.time_estimates = { enabled: flags.enable_time_estimates }; + if (flags.enable_tags !== undefined) features.tags = { enabled: flags.enable_tags }; + if (flags.enable_checklists !== undefined) features.checklists = { enabled: flags.enable_checklists }; + if (flags.enable_custom_fields !== undefined) features.custom_fields = { enabled: flags.enable_custom_fields }; + if (flags.enable_remap_dependencies !== undefined) features.remap_dependencies = { enabled: flags.enable_remap_dependencies }; + if (flags.enable_dependency_warning !== undefined) features.dependency_warning = { enabled: flags.enable_dependency_warning }; + if (flags.enable_portfolios !== undefined) features.portfolios = { enabled: flags.enable_portfolios }; + if (flags.enable_sprints !== undefined) features.sprints = { enabled: flags.enable_sprints }; + if (flags.enable_points !== undefined) features.points = { enabled: flags.enable_points }; + const params: any = { name }; + if (flags.multiple_assignees !== undefined) params.multiple_assignees = flags.multiple_assignees; + if (Object.keys(features).length > 0) params.features = features; + const result = await spacesClient.createSpace(workspace_id, params); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error creating space:', error); + return { + content: [{ type: 'text', text: `Error creating space: ${error.message}` }], + isError: true + }; + } + } + ); + + // Register delete_space tool + server.tool( + 'delete_space', + 'Permanently delete a ClickUp space. This is destructive — the space, its folders, lists, and tasks are removed. There is no undo.', + { space_id: z.string().describe('The ID of the space to delete') }, + async ({ space_id }) => { + try { + await spacesClient.deleteSpace(space_id); + return { + content: [{ type: 'text', text: `Space ${space_id} deleted` }] + }; + } catch (error: any) { + console.error('Error deleting space:', error); + return { + content: [{ type: 'text', text: `Error deleting space: ${error.message}` }], + isError: true + }; + } + } + ); } From 635e14103e69af4cb835318ce3773f6e144fc922 Mon Sep 17 00:00:00 2001 From: Esteban-Rod Date: Wed, 29 Apr 2026 13:34:29 -0500 Subject: [PATCH 3/4] feat: custom fields CRUD, move_task_to_list, get_list_templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five additional tools to close the remaining MCP coverage gaps versus the REST API: - get_custom_fields: list-level field definitions (UUIDs, type_config with drop_down option UUIDs). Cache once per session before assigning values to tasks. - create_custom_field: create a new field on a list. Supports the most common types (drop_down, labels, text, number, date, currency, etc.). Note: ClickUp returns {"id": null} on success — refetch via get_custom_fields to retrieve the actual UUID. - set_custom_field_value: assign a value to a field on a task. Documents the input/output asymmetry (UUID in, orderindex out) for drop_down fields. Pass value=null to clear. - remove_custom_field_value: explicit DELETE alternative to value=null. - move_task_to_list: v3 endpoint (PUT /api/v3/workspaces/{ws}/tasks/ {id}/home_list/{list_id}). Uses the underlying axios instance to call the v3 host directly (the client baseURL is v2). Only root tasks can be moved — sub-tasks must be promoted first. - get_list_templates: discover template_id values for the existing create_list_from_template_in_folder/space tools. These complete the planned coverage in extensions/it/workstreams/clickup-mcp-fork-patches.md. --- src/clickup-client/lists.ts | 37 ++++++++++ src/clickup-client/tasks.ts | 47 ++++++++++++ src/tools/task-tools.ts | 143 ++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) diff --git a/src/clickup-client/lists.ts b/src/clickup-client/lists.ts index 46537b5..c83ab00 100644 --- a/src/clickup-client/lists.ts +++ b/src/clickup-client/lists.ts @@ -136,6 +136,43 @@ export class ListsClient { async createListFromTemplateInSpace(spaceId: string, templateId: string, params: CreateListParams): Promise { return this.client.post(`/space/${spaceId}/list/template/${templateId}`, params); } + + /** + * List the list templates accessible to a workspace. + * Endpoint: GET /team/{workspace_id}/list_template + * @param workspaceId The workspace (team) ID + * @returns The list of available list templates + */ + async getListTemplates(workspaceId: string): Promise<{ templates: Array<{ id: string; name: string }> }> { + return this.client.get(`/team/${workspaceId}/list_template`); + } + + /** + * Get the custom fields defined on a list (list-level fields). + * @param listId The ID of the list + * @returns The list of custom fields with their type_config (drop_down options, etc.) + */ + async getCustomFields(listId: string): Promise<{ fields: Array }> { + return this.client.get(`/list/${listId}/field`); + } + + /** + * Create a new custom field on a list. + * Note: ClickUp's API returns {"id": null} on success — fetch the field + * via getCustomFields after creation to retrieve the assigned UUID. + * @param listId The ID of the list + * @param params The custom field definition + * @returns API response (id may be null — refetch to get the real UUID) + */ + async createCustomField(listId: string, params: CreateCustomFieldParams): Promise<{ id: string | null }> { + return this.client.post(`/list/${listId}/field`, params); + } +} + +export interface CreateCustomFieldParams { + name: string; + type: 'drop_down' | 'labels' | 'text' | 'short_text' | 'number' | 'currency' | 'date' | 'checkbox' | 'url' | 'email' | 'phone' | 'rating' | 'progress' | 'users' | 'emoji' | 'location' | 'tasks' | 'manual_progress' | 'automatic_progress'; + type_config?: any; } export const createListsClient = (client: ClickUpClient): ListsClient => { diff --git a/src/clickup-client/tasks.ts b/src/clickup-client/tasks.ts index 9c4df14..3bcccdb 100644 --- a/src/clickup-client/tasks.ts +++ b/src/clickup-client/tasks.ts @@ -169,6 +169,53 @@ export class TasksClient { return this.client.delete(`/task/${taskId}`); } + /** + * Set the value of a custom field on a task. + * For drop_down fields, value is the option UUID. + * For labels fields, value is an array of option UUIDs. + * For text/number/date/etc., the type-appropriate primitive. + * Pass null to clear the field. + * GOTCHA: API accepts UUIDs (or arrays of UUIDs for labels) on input but + * returns the option's orderindex (integer) when reading the task back. + * @param taskId The ID of the task + * @param fieldId The UUID of the custom field + * @param value The value to set (UUID for drop_down, array for labels, primitive otherwise, or null to clear) + * @returns Success message + */ + async setCustomFieldValue(taskId: string, fieldId: string, value: any): Promise<{ success: boolean }> { + return this.client.post(`/task/${taskId}/field/${fieldId}`, { value }); + } + + /** + * Remove (clear) the value of a custom field on a task. + * Equivalent to setCustomFieldValue with value=null but uses the explicit + * DELETE endpoint. + * @param taskId The ID of the task + * @param fieldId The UUID of the custom field + * @returns Success message + */ + async removeCustomFieldValue(taskId: string, fieldId: string): Promise<{ success: boolean }> { + return this.client.delete(`/task/${taskId}/field/${fieldId}`); + } + + /** + * Move a task to another list. Uses the v3 endpoint + * (PUT /api/v3/workspaces/{workspace_id}/tasks/{task_id}/home_list/{list_id}). + * Note: only root tasks can be moved — sub-tasks must be promoted to root first. + * @param workspaceId The workspace (team) ID + * @param taskId The ID of the task to move + * @param listId The ID of the destination list + * @returns The moved task + */ + async moveTaskToList(workspaceId: string, taskId: string, listId: string): Promise { + const axiosInstance = this.client.getAxiosInstance(); + const response = await axiosInstance.put( + `https://api.clickup.com/api/v3/workspaces/${workspaceId}/tasks/${taskId}/home_list/${listId}`, + {} + ); + return response.data; + } + /** * Get subtasks of a specific task * @param taskId The ID of the task to get subtasks for diff --git a/src/tools/task-tools.ts b/src/tools/task-tools.ts index c08c849..8d6e68a 100644 --- a/src/tools/task-tools.ts +++ b/src/tools/task-tools.ts @@ -511,4 +511,147 @@ export function setupTaskTools(server: McpServer): void { } } ); + + // Templates discovery + server.tool( + 'get_list_templates', + 'List the list templates available to a ClickUp workspace. Use this to find a template_id before calling create_list_from_template_in_folder/space.', + { + workspace_id: z.string().describe('The workspace (team) ID') + }, + async ({ workspace_id }) => { + try { + const result = await listsClient.getListTemplates(workspace_id); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error getting list templates:', error); + return { + content: [{ type: 'text', text: `Error getting list templates: ${error.message}` }], + isError: true + }; + } + } + ); + + // Custom fields — list-level definition + task-level value + server.tool( + 'get_custom_fields', + 'Get the custom fields defined on a list (list-level fields). Returns each field with its UUID, type, and type_config (drop_down options with their UUIDs, etc.). Cache these UUIDs once per session before assigning values to tasks.', + { + list_id: z.string().describe('The ID of the list') + }, + async ({ list_id }) => { + try { + const result = await listsClient.getCustomFields(list_id); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error getting custom fields:', error); + return { + content: [{ type: 'text', text: `Error getting custom fields: ${error.message}` }], + isError: true + }; + } + } + ); + + server.tool( + 'create_custom_field', + 'Create a new custom field on a list (list-level scope). Common types: drop_down, labels, text, number, date, checkbox, url, currency, rating. For drop_down/labels, pass type_config with an "options" array of {name, color, orderindex}. NOTE: ClickUp returns {"id": null} on success — call get_custom_fields after creation to retrieve the actual UUID.', + { + list_id: z.string().describe('The ID of the list to add the custom field to'), + name: z.string().describe('The display name of the custom field'), + type: z.enum(['drop_down', 'labels', 'text', 'short_text', 'number', 'currency', 'date', 'checkbox', 'url', 'email', 'phone', 'rating', 'progress', 'users', 'emoji', 'location', 'tasks', 'manual_progress', 'automatic_progress']).describe('The custom field type'), + type_config: z.record(z.string(), z.any()).optional().describe('Type-specific configuration. For drop_down/labels: {"options": [{"name": "X", "color": "#ff7800", "orderindex": 0}, ...]}. For currency: {"currency_type": "CHF", "precision": 2}. For rating: {"count": 5}. Optional for simple types like text/number/checkbox.') + }, + async ({ list_id, name, type, type_config }) => { + try { + const result = await listsClient.createCustomField(list_id, { name, type, type_config }); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error creating custom field:', error); + return { + content: [{ type: 'text', text: `Error creating custom field: ${error.message}` }], + isError: true + }; + } + } + ); + + server.tool( + 'set_custom_field_value', + 'Set the value of a custom field on a task. For drop_down: pass the option UUID as value. For labels: pass an array of option UUIDs. For text/number/date/url/etc: pass the appropriate primitive. Pass value=null to clear. GOTCHA: API accepts UUID(s) on input but returns option orderindex (integer) when reading task back.', + { + task_id: z.string().describe('The ID of the task'), + field_id: z.string().describe('The UUID of the custom field (from get_custom_fields)'), + value: z.any().describe('Value to set: option UUID for drop_down, array of UUIDs for labels, primitive for other types, or null to clear') + }, + async ({ task_id, field_id, value }) => { + try { + const result = await tasksClient.setCustomFieldValue(task_id, field_id, value); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error setting custom field value:', error); + return { + content: [{ type: 'text', text: `Error setting custom field value: ${error.message}` }], + isError: true + }; + } + } + ); + + server.tool( + 'remove_custom_field_value', + 'Remove (clear) the value of a custom field on a task. Equivalent to set_custom_field_value with value=null but uses the explicit DELETE endpoint.', + { + task_id: z.string().describe('The ID of the task'), + field_id: z.string().describe('The UUID of the custom field') + }, + async ({ task_id, field_id }) => { + try { + const result = await tasksClient.removeCustomFieldValue(task_id, field_id); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error removing custom field value:', error); + return { + content: [{ type: 'text', text: `Error removing custom field value: ${error.message}` }], + isError: true + }; + } + } + ); + + // Move task between lists (v3 endpoint) + server.tool( + 'move_task_to_list', + 'Move a task to another list using the v3 endpoint (PUT /api/v3/workspaces/{ws}/tasks/{id}/home_list/{list_id}). Only root tasks can be moved — sub-tasks must be promoted to root first (the API will reject sub-task moves with "Only root tasks can be moved").', + { + workspace_id: z.string().describe('The workspace (team) ID'), + task_id: z.string().describe('The ID of the task to move (must be a root task, not a sub-task)'), + list_id: z.string().describe('The ID of the destination list') + }, + async ({ workspace_id, task_id, list_id }) => { + try { + const result = await tasksClient.moveTaskToList(workspace_id, task_id, list_id); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error moving task to list:', error); + return { + content: [{ type: 'text', text: `Error moving task to list: ${error.message}` }], + isError: true + }; + } + } + ); } From c48ca657906ced6bd0157a18733d076c327a07c9 Mon Sep 17 00:00:00 2001 From: Esteban-Rod Date: Wed, 29 Apr 2026 17:32:35 -0500 Subject: [PATCH 4/4] feat: add get_folders tool to list folders in a space The client already exposed FoldersClient.getFoldersFromSpace but it wasn't surfaced as a tool. Without it, listing folders required a REST fallback (curl + Bitwarden token), which forced the clickup-audit skill to step out of MCP-only execution. Maps to GET /space/{id}/folder. --- src/tools/task-tools.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/tools/task-tools.ts b/src/tools/task-tools.ts index 8d6e68a..c7a3d43 100644 --- a/src/tools/task-tools.ts +++ b/src/tools/task-tools.ts @@ -206,6 +206,30 @@ export function setupTaskTools(server: McpServer): void { } ); + server.tool( + 'get_folders', + 'List all folders in a ClickUp space. Returns folder id, name, and metadata for each folder. Use get_lists with container_type="folder" to drill into a folder afterwards.', + { + space_id: z.string().describe('The ID of the space to list folders from'), + archived: z.boolean().optional().describe('Whether to include archived folders (default: false)') + }, + async ({ space_id, archived }) => { + try { + const params = archived !== undefined ? { archived } : undefined; + const result = await foldersClient.getFoldersFromSpace(space_id, params as any); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error: any) { + console.error('Error getting folders:', error); + return { + content: [{ type: 'text', text: `Error getting folders: ${error.message}` }], + isError: true + }; + } + } + ); + server.tool( 'create_folder', 'Create a new folder in a ClickUp space with the specified name.',