@djclarkson/clickup-mcp-server
Version:
ClickUp MCP Server - Enhanced fork with task dependency management and parameter fixes
338 lines (337 loc) • 14.2 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
* SPDX-License-Identifier: MIT
*
* ClickUp Task Service - Dependencies Module
*
* Handles task dependency operations in ClickUp, including:
* - Creating and removing task dependencies
* - Retrieving dependency information
* - Managing blocking/waiting relationships
*/
import { ErrorCode, ClickUpServiceError } from '../base.js';
import { TaskServiceComments } from './task-comments.js';
/**
* Task Dependencies Service class
*/
export class TaskServiceDependencies extends TaskServiceComments {
/**
* Add a dependency between two tasks
* @param params Dependency parameters
* @returns ServiceResponse with the created dependency
*/
async addTaskDependency(params) {
try {
this.logOperation('addTaskDependency', { params });
// Resolve task IDs
const taskId = await this.resolveTaskId(params.taskId, params.taskName, params.listName);
const dependsOnTaskId = await this.resolveTaskId(params.dependsOnTaskId, params.dependsOnTaskName, params.dependsOnListName);
if (!taskId) {
throw new ClickUpServiceError('Unable to resolve task ID from provided parameters', ErrorCode.NOT_FOUND);
}
if (!dependsOnTaskId) {
throw new ClickUpServiceError('Unable to resolve dependency task ID from provided parameters', ErrorCode.NOT_FOUND);
}
// Check for circular dependencies
await this.checkCircularDependency(taskId, dependsOnTaskId);
// Create the dependency using the correct API endpoint
// ClickUp API uses /task/{task_id}/link/{depends_on_id}
const endpoint = `/task/${taskId}/link/${dependsOnTaskId}`;
const response = await this.client.post(endpoint, {});
this.logOperation('addTaskDependency', {
success: true,
taskId,
dependsOnTaskId
});
// The API returns the updated task, extract the dependency that was added
const addedDependency = {
task_id: taskId,
depends_on: dependsOnTaskId,
type: 1, // waiting_on
date_created: new Date().getTime().toString(),
userid: '', // Will be filled by API
workspace_id: this.teamId,
chain_id: null
};
return {
data: addedDependency,
success: true
};
}
catch (error) {
const serviceError = this.handleError(error, 'Failed to add task dependency');
this.logOperation('addTaskDependency', {
error: serviceError.message,
params
});
return {
data: null,
success: false,
error: serviceError
};
}
}
/**
* Remove a dependency between two tasks
* @param params Dependency parameters
* @returns ServiceResponse indicating success
*/
async removeTaskDependency(params) {
try {
this.logOperation('removeTaskDependency', { params });
// Resolve task IDs
const taskId = await this.resolveTaskId(params.taskId, params.taskName, params.listName);
const dependencyTaskId = await this.resolveTaskId(params.dependencyTaskId, params.dependencyTaskName, params.dependencyListName);
if (!taskId) {
throw new ClickUpServiceError('Unable to resolve task ID from provided parameters', ErrorCode.NOT_FOUND);
}
if (!dependencyTaskId) {
throw new ClickUpServiceError('Unable to resolve dependency task ID from provided parameters', ErrorCode.NOT_FOUND);
}
// Remove the dependency using the correct API endpoint
// ClickUp API uses DELETE /task/{task_id}/link/{depends_on_id}
const endpoint = `/task/${taskId}/link/${dependencyTaskId}`;
await this.client.delete(endpoint);
this.logOperation('removeTaskDependency', {
success: true,
taskId,
dependencyTaskId
});
return {
data: null,
success: true
};
}
catch (error) {
const serviceError = this.handleError(error, 'Failed to remove task dependency');
this.logOperation('removeTaskDependency', {
error: serviceError.message,
params
});
return {
data: null,
success: false,
error: serviceError
};
}
}
/**
* Get all dependencies for a task
* @param params Task parameters
* @returns ServiceResponse with task dependencies
*/
async getTaskDependencies(params) {
try {
this.logOperation('getTaskDependencies', { params });
// Resolve task ID
const taskId = await this.resolveTaskId(params.taskId, params.taskName, params.listName);
if (!taskId) {
throw new ClickUpServiceError('Unable to resolve task ID from provided parameters', ErrorCode.NOT_FOUND);
}
// Get the task with dependencies
const task = await this.getTask(taskId);
if (!task) {
throw new ClickUpServiceError('Failed to retrieve task information', ErrorCode.NOT_FOUND);
}
// Process dependencies
const dependencies = {
task: {
id: task.id,
name: task.name
},
dependencies: {
waiting_on: [],
blocking: []
}
};
// Get detailed information for each dependency
// Dependencies in ClickUp are stored as an array of objects
if (task.dependencies && Array.isArray(task.dependencies) && task.dependencies.length > 0) {
// Process each dependency object
for (const dep of task.dependencies) {
// Check if this is a "waiting_on" dependency (this task depends on another)
if (dep.task_id === task.id && dep.depends_on) {
try {
const depTask = await this.getTask(dep.depends_on);
if (depTask) {
const depInfo = {
task_id: depTask.id,
task_name: depTask.name,
status: depTask.status.status,
list: {
id: depTask.list.id,
name: depTask.list.name
}
};
dependencies.dependencies.waiting_on.push(depInfo);
}
}
catch (error) {
this.logger.warn(`Failed to get dependency task ${dep.depends_on}:`, error);
}
}
}
// To find blocking dependencies, we need to search for tasks that depend on this one
// This would require searching all tasks, which is expensive
// For now, we'll leave blocking empty unless specifically implemented
}
// Include subtask dependencies if requested
if (params.includeSubtasks && task.subtasks) {
for (const subtask of task.subtasks) {
if (subtask.dependencies && subtask.dependencies.length > 0) {
const subtaskDeps = await this.getTaskDependencies({
taskId: subtask.id,
includeSubtasks: false
});
if (subtaskDeps.success && subtaskDeps.data) {
dependencies.dependencies.waiting_on.push(...subtaskDeps.data.dependencies.waiting_on);
dependencies.dependencies.blocking.push(...subtaskDeps.data.dependencies.blocking);
}
}
}
}
this.logOperation('getTaskDependencies', {
success: true,
taskId,
dependencyCount: dependencies.dependencies.waiting_on.length +
dependencies.dependencies.blocking.length
});
return {
data: dependencies,
success: true
};
}
catch (error) {
const serviceError = this.handleError(error, 'Failed to get task dependencies');
this.logOperation('getTaskDependencies', {
error: serviceError.message,
params
});
return {
data: null,
success: false,
error: serviceError
};
}
}
/**
* Check for circular dependencies
* @param taskId Task that would be blocked
* @param dependsOnTaskId Task that would be blocking
* @throws ClickUpServiceError if circular dependency detected
*/
async checkCircularDependency(taskId, dependsOnTaskId) {
// Simple check: prevent self-dependency
if (taskId === dependsOnTaskId) {
throw new ClickUpServiceError('Cannot create self-dependency', ErrorCode.VALIDATION);
}
// Check if dependsOnTaskId already depends on taskId
const depsResponse = await this.getTaskDependencies({ taskId: dependsOnTaskId });
if (depsResponse.success && depsResponse.data) {
const waitingOnIds = depsResponse.data.dependencies.waiting_on.map(d => d.task_id);
if (waitingOnIds.includes(taskId)) {
throw new ClickUpServiceError('Circular dependency detected: Target task already depends on source task', ErrorCode.VALIDATION);
}
}
}
/**
* Add multiple dependencies in bulk
* @param params Bulk dependency parameters
* @returns ServiceResponse with results for each operation
*/
async addBulkDependencies(params) {
try {
this.logOperation('addBulkDependencies', {
count: params.dependencies.length,
options: params.options
});
const results = {
successful: [],
failed: [],
summary: { total: 0, success: 0, failed: 0 }
};
// Process each dependency item
for (const depItem of params.dependencies) {
const { taskId, dependsOn } = depItem;
// Process each dependency for this task
for (const dependsOnTaskId of dependsOn) {
results.summary.total++;
try {
// Check for circular dependencies
await this.checkCircularDependency(taskId, dependsOnTaskId);
// Create the dependency
const endpoint = `/task/${taskId}/dependency`;
await this.client.post(endpoint, {
depends_on: dependsOnTaskId,
dependency_type: 0 // waiting_on
});
results.successful.push({
taskId,
dependsOn: dependsOnTaskId,
success: true
});
results.summary.success++;
}
catch (error) {
const errorMessage = error.message || 'Unknown error';
results.failed.push({
taskId,
dependsOn: dependsOnTaskId,
error: errorMessage
});
results.summary.failed++;
// Stop processing if continueOnError is false
if (!params.options?.continueOnError) {
break;
}
}
}
// Stop processing if we had an error and continueOnError is false
if (!params.options?.continueOnError && results.failed.length > 0) {
break;
}
}
this.logOperation('addBulkDependencies', {
success: true,
summary: results.summary
});
return {
data: results,
success: true
};
}
catch (error) {
const serviceError = this.handleError(error, 'Failed to add bulk dependencies');
this.logOperation('addBulkDependencies', {
error: serviceError.message,
params
});
return {
data: null,
success: false,
error: serviceError
};
}
}
/**
* Resolve a task ID from various parameter combinations
* @param taskId Direct task ID
* @param taskName Task name to look up
* @param listName List name for context
* @returns Resolved task ID or null
*/
async resolveTaskId(taskId, taskName, listName) {
if (taskId) {
return taskId;
}
if (taskName) {
const searchParams = { taskName };
if (listName) {
searchParams.listName = listName;
}
const result = await this.findTaskByName(taskName, listName);
return result ? result.id : null;
}
return null;
}
}