Skip to main content
Glama

ClickUp MCP

by TwoFeetUp
consolidated-handlers.ts21.3 kB
/** * SPDX-FileCopyrightText: © 2025 Sjoerd Tiemensma * SPDX-License-Identifier: MIT * * ClickUp MCP Consolidated Task Handlers * * Implements action-based routing for consolidated task tools. * Routes to existing handlers where possible, leveraging established patterns. */ import { Logger } from '../../logger.js'; import { createTaskHandler, updateTaskHandler, deleteTaskHandler, moveTaskHandler, duplicateTaskHandler, getTaskHandler, getTasksHandler, getTaskCommentsHandler, createTaskCommentHandler, getWorkspaceTasksHandler, getTaskId } from './handlers.js'; import { handleGetTaskTimeEntries, handleStartTimeTracking, handleStopTimeTracking, handleAddTimeEntry, handleDeleteTimeEntry, handleGetCurrentTimeEntry } from './time-tracking.js'; import { handleAttachTaskFile } from './attachments.js'; import { formatResponse, paginate, normalizeArray } from '../../utils/response-formatter.js'; import { TaskService } from '../../services/clickup/task/task-service.js'; import { clickUpServices } from '../../services/shared.js'; import { sponsorService } from '../../utils/sponsor-service.js'; const logger = new Logger('ConsolidatedHandlers'); const { task: taskService } = clickUpServices; //============================================================================= // DATE CONVERSION HELPER //============================================================================= /** * Convert date string to Unix timestamp in milliseconds * Accepts: ISO dates, human-readable dates, or timestamps */ function convertToTimestamp(dateInput: string | number): string { // Already a timestamp if (typeof dateInput === 'number' || /^\d+$/.test(dateInput)) { const timestamp = typeof dateInput === 'string' ? parseInt(dateInput) : dateInput; // If it's in seconds, convert to milliseconds return timestamp < 10000000000 ? (timestamp * 1000).toString() : timestamp.toString(); } // Parse date string const date = new Date(dateInput); if (isNaN(date.getTime())) { logger.warn(`Invalid date format: ${dateInput}, returning as-is`); return dateInput.toString(); } return date.getTime().toString(); } //============================================================================= // MANAGE TASK HANDLER - consolidates create/update/delete/move/duplicate //============================================================================= /** * Handler for manage_task tool * Routes to specific handlers based on action parameter */ export async function handleManageTask(params: any) { const { action } = params; logger.info(`Handling manage_task action: ${action}`, { action }); try { switch (action) { case 'create': const createResult = await createTaskHandler(params); return sponsorService.createResponse(createResult); case 'update': // Update requires task identification if (!params.taskId && !params.taskName && !params.customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } const updateResult = await updateTaskHandler(taskService, params); return sponsorService.createResponse(updateResult); case 'delete': // Delete requires task identification if (!params.taskId && !params.taskName && !params.customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } const deleteResult = await deleteTaskHandler(params); return sponsorService.createResponse(deleteResult); case 'move': // Move requires task identification and target list if (!params.taskId && !params.taskName && !params.customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } if (!params.targetListId && !params.targetListName && !params.listId && !params.listName) { throw new Error('Target list required: provide targetListId, targetListName, listId, or listName'); } // Map targetListId/targetListName to the parameter names expected by moveTaskHandler const moveParams = { ...params, listId: params.targetListId || params.listId, listName: params.targetListName || params.listName }; const moveResult = await moveTaskHandler(moveParams); return sponsorService.createResponse(moveResult); case 'duplicate': // Duplicate requires task identification if (!params.taskId && !params.taskName && !params.customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } // Map targetListId/targetListName to the parameter names expected by duplicateTaskHandler const dupParams = { ...params, listId: params.targetListId || params.listId, listName: params.targetListName || params.listName }; const dupResult = await duplicateTaskHandler(dupParams); return sponsorService.createResponse(dupResult); default: throw new Error(`Invalid action: ${action}. Must be one of: create, update, delete, move, duplicate`); } } catch (error) { logger.error(`Error handling manage_task action: ${action}`, { error: (error as Error).message }); return sponsorService.createErrorResponse(error as Error); } } //============================================================================= // SEARCH TASKS HANDLER - consolidates get_task/get_tasks/get_workspace_tasks //============================================================================= /** * Handler for search_tasks tool * Routes to single task, list, or workspace search based on parameters */ export async function handleSearchTasks(params: any) { const { taskId, taskName, customTaskId, listId, listName, list_ids, folder_ids, space_ids, detail_level = 'standard', fields, offset = 0, limit = 50, include_subtasks, include_empty_custom_fields = false } = params; logger.info('Handling search_tasks', { taskId, taskName, listId, listName }); try { // Single task retrieval takes priority if (taskId || taskName || customTaskId) { const task = await getTaskHandler({ taskId, taskName, customTaskId, listName, subtasks: include_subtasks }); const formattedTask = formatResponse(task, { detailLevel: detail_level, fields, includeMetadata: true, includeEmptyCustomFields: include_empty_custom_fields }); return sponsorService.createResponse(formattedTask); } // List-based search if (listId || listName) { const tasks = await getTasksHandler({ listId, listName, ...params }); // Apply pagination const tasksArray = Array.isArray(tasks) ? tasks : (tasks && typeof tasks === 'object' && 'tasks' in tasks) ? (tasks as any).tasks || [] : []; const { items, pagination } = paginate( tasksArray, offset, limit ); // Auto-downgrade detail_level if too many results let effectiveDetailLevel = detail_level; if (detail_level === 'detailed' && items.length > 10) { effectiveDetailLevel = 'standard'; logger.info(`Auto-downgraded detail_level from 'detailed' to 'standard' (${items.length} tasks found, limit is 10)`); } // Format response with pagination const response = formatResponse(items, { detailLevel: effectiveDetailLevel, fields, includeMetadata: true, includeEmptyCustomFields: include_empty_custom_fields }); if (response.metadata) { response.metadata.pagination = pagination; if (effectiveDetailLevel !== detail_level) { response.metadata.note = `Detail level auto-downgraded from 'detailed' to 'standard' due to ${items.length} tasks (limit: 10). Use standard or minimal for large result sets.`; } } return sponsorService.createResponse(response); } // Workspace-wide search requires at least one filter // Support both assignee_ids (new) and assignees (legacy) for backward compatibility const assigneeIds = params.assignee_ids || params.assignees; if (list_ids || folder_ids || space_ids || params.tags || params.statuses || assigneeIds || params.date_created_gt || params.date_created_lt || params.date_updated_gt || params.date_updated_lt || params.due_date_gt || params.due_date_lt) { // Map assignee_ids to assignees for the handler const searchParams = { ...params }; if (assigneeIds) { searchParams.assignees = assigneeIds; } // Auto-convert date strings to timestamps if (searchParams.date_created_gt) searchParams.date_created_gt = convertToTimestamp(searchParams.date_created_gt); if (searchParams.date_created_lt) searchParams.date_created_lt = convertToTimestamp(searchParams.date_created_lt); if (searchParams.date_updated_gt) searchParams.date_updated_gt = convertToTimestamp(searchParams.date_updated_gt); if (searchParams.date_updated_lt) searchParams.date_updated_lt = convertToTimestamp(searchParams.date_updated_lt); if (searchParams.due_date_gt) searchParams.due_date_gt = convertToTimestamp(searchParams.due_date_gt); if (searchParams.due_date_lt) searchParams.due_date_lt = convertToTimestamp(searchParams.due_date_lt); const workspaceTasks = await getWorkspaceTasksHandler(taskService, searchParams); // Extract tasks array from response const tasks = Array.isArray(workspaceTasks) ? workspaceTasks : workspaceTasks.tasks || workspaceTasks.summaries || []; // Apply pagination const { items, pagination } = paginate(tasks, offset, limit); // Auto-downgrade detail_level if too many results let effectiveDetailLevel = detail_level; if (detail_level === 'detailed' && items.length > 10) { effectiveDetailLevel = 'standard'; logger.info(`Auto-downgraded detail_level from 'detailed' to 'standard' (${items.length} tasks found, limit is 10)`); } // Format response const response = formatResponse(items, { detailLevel: effectiveDetailLevel, fields, includeMetadata: true, includeEmptyCustomFields: include_empty_custom_fields }); if (response.metadata) { response.metadata.pagination = pagination; if (effectiveDetailLevel !== detail_level) { response.metadata.note = `Detail level auto-downgraded from 'detailed' to 'standard' due to ${items.length} tasks (limit: 10). Use standard or minimal for large result sets.`; } } return sponsorService.createResponse(response); } // Default behavior: if no parameters provided, search workspace with default filters // This follows MCP design principle: tools should "just work" with sensible defaults logger.info('No specific search parameters provided, defaulting to workspace-wide search'); const workspaceTasks = await getWorkspaceTasksHandler(taskService, { ...params, detail_level: detail_level || 'standard' }); // Extract tasks array from response const tasks = Array.isArray(workspaceTasks) ? workspaceTasks : workspaceTasks.tasks || workspaceTasks.summaries || []; // Apply pagination const { items, pagination } = paginate(tasks, offset, limit); // Auto-downgrade detail_level if too many results let effectiveDetailLevel = detail_level; if (detail_level === 'detailed' && items.length > 10) { effectiveDetailLevel = 'standard'; logger.info(`Auto-downgraded detail_level from 'detailed' to 'standard' (${items.length} tasks found, limit is 10)`); } // Format response const response = formatResponse(items, { detailLevel: effectiveDetailLevel, fields, includeMetadata: true, includeEmptyCustomFields: include_empty_custom_fields }); if (response.metadata) { response.metadata.pagination = pagination; let note = 'Showing recent tasks from workspace. Add filters (assignees, tags, statuses, etc.) to narrow results.'; if (effectiveDetailLevel !== detail_level) { note += ` Detail level auto-downgraded from 'detailed' to 'standard' due to ${items.length} tasks (limit: 10).`; } response.metadata.note = note; } return sponsorService.createResponse(response); } catch (error) { logger.error('Error handling search_tasks', { error: (error as Error).message }); return sponsorService.createErrorResponse(error as Error); } } //============================================================================= // TASK COMMENTS HANDLER - consolidates comment operations //============================================================================= /** * Handler for task_comments tool * Routes to get or create comment based on action */ export async function handleTaskComments(params: any) { const { action, taskId, taskName, customTaskId, listName } = params; logger.info(`Handling task_comments action: ${action}`, { action }); try { // Validate task identification if (!taskId && !taskName && !customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } switch (action) { case 'get': // Get comments for task const getResult = await getTaskCommentsHandler({ taskId, taskName, customTaskId, listName, start: params.start, startId: params.startId }); return sponsorService.createResponse(getResult); case 'create': // Create new comment if (!params.commentText) { throw new Error('Comment text is required for create action'); } const createResult = await createTaskCommentHandler({ taskId, taskName, customTaskId, listName, commentText: params.commentText, notifyAll: params.notifyAll, assignee: params.assignee }); return sponsorService.createResponse(createResult); default: throw new Error(`Invalid action: ${action}. Must be one of: get, create`); } } catch (error) { logger.error(`Error handling task_comments action: ${action}`, { error: (error as Error).message }); return sponsorService.createErrorResponse(error as Error); } } //============================================================================= // TASK TIME TRACKING HANDLER - consolidates all time tracking operations //============================================================================= /** * Handler for task_time_tracking tool * Routes to specific time tracking handlers based on action */ export async function handleTaskTimeTracking(params: any) { const { action, taskId, taskName, customTaskId, listName } = params; logger.info(`Handling task_time_tracking action: ${action}`, { action }); try { switch (action) { case 'get_entries': // Get time entries for task if (!taskId && !taskName && !customTaskId) { throw new Error('Task identification required for get_entries action'); } const entriesResult = await handleGetTaskTimeEntries({ taskId, taskName, customTaskId, listName, startDate: params.startDate, endDate: params.endDate }); return sponsorService.createResponse(entriesResult); case 'start': // Start time tracking on task if (!taskId && !taskName && !customTaskId) { throw new Error('Task identification required for start action'); } const startResult = await handleStartTimeTracking({ taskId, taskName, customTaskId, listName, description: params.description, billable: params.billable, tags: params.tags }); return sponsorService.createResponse(startResult); case 'stop': // Stop currently running timer const stopResult = await handleStopTimeTracking({ description: params.description, tags: params.tags }); return sponsorService.createResponse(stopResult); case 'add_entry': // Add manual time entry if (!taskId && !taskName && !customTaskId) { throw new Error('Task identification required for add_entry action'); } if (!params.start) { throw new Error('Start time is required for add_entry action'); } if (!params.duration) { throw new Error('Duration is required for add_entry action'); } const addResult = await handleAddTimeEntry({ taskId, taskName, customTaskId, listName, start: params.start, duration: params.duration, description: params.description, billable: params.billable, tags: params.tags }); return sponsorService.createResponse(addResult); case 'delete_entry': // Delete time entry if (!params.timeEntryId) { throw new Error('Time entry ID is required for delete_entry action'); } const deleteResult = await handleDeleteTimeEntry({ timeEntryId: params.timeEntryId }); return sponsorService.createResponse(deleteResult); case 'get_current': // Get currently running timer const currentResult = await handleGetCurrentTimeEntry(); return sponsorService.createResponse(currentResult); default: throw new Error(`Invalid action: ${action}. Must be one of: get_entries, start, stop, add_entry, delete_entry, get_current`); } } catch (error) { logger.error(`Error handling task_time_tracking action: ${action}`, { error: (error as Error).message }); return sponsorService.createErrorResponse(error as Error); } } //============================================================================= // ATTACH FILE HANDLER - routes to existing attachments implementation //============================================================================= /** * Handler for attach_file_to_task tool * Routes to existing file attachment handler from attachments.ts */ export async function handleAttachFileToTaskConsolidated(params: any) { const { taskId, taskName, customTaskId, listName, attachmentUrl } = params; logger.info('Handling attach_file_to_task', { taskId, taskName }); try { // Validate task identification if (!taskId && !taskName && !customTaskId) { throw new Error('Task identification required: provide taskId, taskName, or customTaskId'); } // Validate attachment URL if (!attachmentUrl) { throw new Error('Attachment URL is required'); } // Route to existing attachment handler with consolidated parameters const attachResult = await handleAttachTaskFile({ taskId, taskName, customTaskId, listName, attachment_url: attachmentUrl // Note: handleAttachTaskFile uses attachment_url }); return sponsorService.createResponse(attachResult); } catch (error) { logger.error('Error handling attach_file_to_task', { error: (error as Error).message }); return sponsorService.createErrorResponse(error as Error); } } //============================================================================= // UNIFIED HANDLER ROUTER //============================================================================= /** * Route tool calls to appropriate consolidated handlers * Dispatcher for all consolidated task tools */ export async function handleConsolidatedTaskTool(toolName: string, params: any) { logger.info(`Routing consolidated task tool: ${toolName}`); try { switch (toolName) { case 'manage_task': return await handleManageTask(params); case 'search_tasks': return await handleSearchTasks(params); case 'task_comments': return await handleTaskComments(params); case 'task_time_tracking': return await handleTaskTimeTracking(params); case 'attach_file_to_task': return await handleAttachFileToTaskConsolidated(params); default: throw new Error(`Unknown consolidated task tool: ${toolName}`); } } catch (error) { logger.error(`Error routing consolidated task tool: ${toolName}`, { error: (error as Error).message }); throw error; } } //============================================================================= // HANDLER EXPORTS //============================================================================= export const consolidatedTaskHandlers = { manage_task: handleManageTask, search_tasks: handleSearchTasks, task_comments: handleTaskComments, task_time_tracking: handleTaskTimeTracking, attach_file_to_task: handleAttachFileToTaskConsolidated };

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/TwoFeetUp/clickup-mcp'

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