Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/clickup-client/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,43 @@ export class ListsClient {
async createListFromTemplateInSpace(spaceId: string, templateId: string, params: CreateListParams): Promise<List> {
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<any> }> {
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 => {
Expand Down
60 changes: 60 additions & 0 deletions src/clickup-client/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,66 @@ 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<Space> {
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<Space> {
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<void> {
await this.client.delete(`/space/${spaceId}`);
}
}

export interface CreateSpaceParams {
name: string;
multiple_assignees?: boolean;
features?: UpdateSpaceParams['features'];
}

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 => {
Expand Down
47 changes: 47 additions & 0 deletions src/clickup-client/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task> {
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
Expand Down
133 changes: 132 additions & 1 deletion src/tools/space-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) }]
};
Expand All @@ -55,4 +55,135 @@ 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
};
}
}
);

// 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
};
}
}
);
}
Loading