ClickUp MCP Server
by v4lheru
Verified
- src
- services
/**
* ClickUp Relationships Service
*
* This service provides functionality for managing task relationships in ClickUp,
* including dependencies, links, attachments, tags, and comments.
*/
import axios, { AxiosInstance } from 'axios';
/**
* Service for interacting with ClickUp's relationship APIs.
* Handles task dependencies, links, attachments, tags, and comments.
*/
export class ClickUpRelationshipsService {
private static instance: ClickUpRelationshipsService;
private apiKey: string;
private teamId: string;
private axiosInstance: AxiosInstance;
/**
* Private constructor to enforce singleton pattern.
* @param apiKey - ClickUp API key
* @param teamId - ClickUp team ID
*/
private constructor(apiKey: string, teamId: string) {
this.apiKey = apiKey;
this.teamId = teamId;
this.axiosInstance = axios.create({
baseURL: 'https://api.clickup.com/api/v2',
headers: {
'Authorization': apiKey,
'Content-Type': 'application/json'
}
});
}
/**
* Initialize the ClickUpRelationshipsService singleton.
* @param apiKey - ClickUp API key
* @param teamId - ClickUp team ID
* @returns The ClickUpRelationshipsService instance
*/
public static initialize(apiKey: string, teamId: string): ClickUpRelationshipsService {
if (!ClickUpRelationshipsService.instance) {
ClickUpRelationshipsService.instance = new ClickUpRelationshipsService(apiKey, teamId);
}
return ClickUpRelationshipsService.instance;
}
/**
* Get the team ID.
* @returns The team ID
*/
public getTeamId(): string {
return this.teamId;
}
// Task Dependencies
/**
* Add a dependency relationship between two tasks.
* @param taskId - ID of the task which is waiting on or blocking another task
* @param dependsOn - ID of the task that must be completed first (optional if dependencyOf is provided)
* @param dependencyOf - ID of the task that's waiting for this task (optional if dependsOn is provided)
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async addTaskDependency(
taskId: string,
dependsOn?: string,
dependencyOf?: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/dependency?${queryParams.toString()}`;
const body = dependsOn ? { depends_on: dependsOn } : { dependency_of: dependencyOf };
const response = await this.axiosInstance.post(url, body);
return response.data;
} catch (error) {
console.error('Error adding task dependency:', error);
throw error;
}
}
/**
* Delete a dependency relationship between two tasks.
* @param taskId - ID of the task
* @param dependsOn - ID of the task that needed to be completed first
* @param dependencyOf - ID of the task that was waiting
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async deleteTaskDependency(
taskId: string,
dependsOn: string,
dependencyOf: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
queryParams.append('depends_on', dependsOn);
queryParams.append('dependency_of', dependencyOf);
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/dependency?${queryParams.toString()}`;
const response = await this.axiosInstance.delete(url);
return response.data;
} catch (error) {
console.error('Error deleting task dependency:', error);
throw error;
}
}
// Task Links
/**
* Add a link between two tasks.
* @param taskId - ID of the task to initiate the link from
* @param linksTo - ID of the task to link to
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async addTaskLink(
taskId: string,
linksTo: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/link/${linksTo}?${queryParams.toString()}`;
const response = await this.axiosInstance.post(url);
return response.data;
} catch (error) {
console.error('Error adding task link:', error);
throw error;
}
}
/**
* Delete a link between two tasks.
* @param taskId - ID of the first task in the link
* @param linksTo - ID of the second task in the link
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async deleteTaskLink(
taskId: string,
linksTo: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/link/${linksTo}?${queryParams.toString()}`;
const response = await this.axiosInstance.delete(url);
return response.data;
} catch (error) {
console.error('Error deleting task link:', error);
throw error;
}
}
// Task Tags
/**
* Add a tag to a task.
* @param taskId - ID of the task
* @param tagName - Name of the tag to add
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async addTagToTask(
taskId: string,
tagName: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/tag/${encodeURIComponent(tagName)}?${queryParams.toString()}`;
const response = await this.axiosInstance.post(url);
return response.data;
} catch (error) {
console.error('Error adding tag to task:', error);
throw error;
}
}
/**
* Remove a tag from a task.
* @param taskId - ID of the task
* @param tagName - Name of the tag to remove
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async removeTagFromTask(
taskId: string,
tagName: string,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/tag/${encodeURIComponent(tagName)}?${queryParams.toString()}`;
const response = await this.axiosInstance.delete(url);
return response.data;
} catch (error) {
console.error('Error removing tag from task:', error);
throw error;
}
}
// Task Comments
/**
* Get comments for a specific task.
* @param taskId - ID of the task
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @param start - Unix timestamp in milliseconds to get comments from a specific date
* @param startId - Comment ID to start pagination from
* @returns The response from the API
*/
public async getTaskComments(
taskId: string,
customTaskIds?: boolean,
teamId?: string,
start?: number,
startId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
if (start) {
queryParams.append('start', start.toString());
}
if (startId) {
queryParams.append('start_id', startId);
}
const url = `/task/${taskId}/comment?${queryParams.toString()}`;
const response = await this.axiosInstance.get(url);
return response.data;
} catch (error) {
console.error('Error getting task comments:', error);
throw error;
}
}
/**
* Create a comment on a task.
* @param taskId - ID of the task
* @param commentText - Content of the comment
* @param assignee - User ID to assign the comment to (optional)
* @param groupAssignee - Group to assign the comment to (optional)
* @param notifyAll - If true, notifications will be sent to everyone including the comment creator
* @param customTaskIds - Whether to use custom task IDs
* @param teamId - Team ID (required when customTaskIds is true)
* @returns The response from the API
*/
public async createTaskComment(
taskId: string,
commentText: string,
assignee?: number,
groupAssignee?: string,
notifyAll: boolean = false,
customTaskIds?: boolean,
teamId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (customTaskIds) {
queryParams.append('custom_task_ids', 'true');
if (teamId) {
queryParams.append('team_id', teamId);
} else {
queryParams.append('team_id', this.teamId);
}
}
const url = `/task/${taskId}/comment?${queryParams.toString()}`;
const body: any = {
comment_text: commentText,
notify_all: notifyAll
};
if (assignee) {
body.assignee = assignee;
}
if (groupAssignee) {
body.group_assignee = groupAssignee;
}
const response = await this.axiosInstance.post(url, body);
return response.data;
} catch (error) {
console.error('Error creating task comment:', error);
throw error;
}
}
// List Comments
/**
* Get comments for a specific list.
* @param listId - ID of the list
* @param start - Unix timestamp in milliseconds to get comments from a specific date
* @param startId - Comment ID to start pagination from
* @returns The response from the API
*/
public async getListComments(
listId: string,
start?: number,
startId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (start) {
queryParams.append('start', start.toString());
}
if (startId) {
queryParams.append('start_id', startId);
}
const url = `/list/${listId}/comment?${queryParams.toString()}`;
const response = await this.axiosInstance.get(url);
return response.data;
} catch (error) {
console.error('Error getting list comments:', error);
throw error;
}
}
/**
* Create a comment on a list.
* @param listId - ID of the list
* @param commentText - Content of the comment
* @param assignee - User ID to assign the comment to
* @param notifyAll - If true, notifications will be sent to everyone including the comment creator
* @returns The response from the API
*/
public async createListComment(
listId: string,
commentText: string,
assignee: number,
notifyAll: boolean = false
): Promise<any> {
try {
const url = `/list/${listId}/comment`;
const body = {
comment_text: commentText,
assignee,
notify_all: notifyAll
};
const response = await this.axiosInstance.post(url, body);
return response.data;
} catch (error) {
console.error('Error creating list comment:', error);
throw error;
}
}
// Chat View Comments
/**
* Get comments from a Chat view.
* @param viewId - ID of the Chat view
* @param start - Unix timestamp in milliseconds to get comments from a specific date
* @param startId - Comment ID to start pagination from
* @returns The response from the API
*/
public async getChatViewComments(
viewId: string,
start?: number,
startId?: string
): Promise<any> {
try {
const queryParams = new URLSearchParams();
if (start) {
queryParams.append('start', start.toString());
}
if (startId) {
queryParams.append('start_id', startId);
}
const url = `/view/${viewId}/comment?${queryParams.toString()}`;
const response = await this.axiosInstance.get(url);
return response.data;
} catch (error) {
console.error('Error getting chat view comments:', error);
throw error;
}
}
/**
* Create a comment on a Chat view.
* @param viewId - ID of the Chat view
* @param commentText - Content of the comment
* @param notifyAll - If true, notifications will be sent to everyone including the comment creator
* @returns The response from the API
*/
public async createChatViewComment(
viewId: string,
commentText: string,
notifyAll: boolean = false
): Promise<any> {
try {
const url = `/view/${viewId}/comment`;
const body = {
comment_text: commentText,
notify_all: notifyAll
};
const response = await this.axiosInstance.post(url, body);
return response.data;
} catch (error) {
console.error('Error creating chat view comment:', error);
throw error;
}
}
// General Comment Operations
/**
* Update an existing comment.
* @param commentId - ID of the comment to update
* @param commentText - New content for the comment
* @param assignee - User ID to assign the comment to
* @param groupAssignee - Group to assign the comment to
* @param resolved - Mark comment as resolved or not
* @returns The response from the API
*/
public async updateComment(
commentId: string,
commentText: string,
assignee: number,
groupAssignee?: number,
resolved: boolean = false
): Promise<any> {
try {
const url = `/comment/${commentId}`;
const body: any = {
comment_text: commentText,
assignee,
resolved
};
if (groupAssignee) {
body.group_assignee = groupAssignee;
}
const response = await this.axiosInstance.put(url, body);
return response.data;
} catch (error) {
console.error('Error updating comment:', error);
throw error;
}
}
/**
* Delete a comment.
* @param commentId - ID of the comment to delete
* @returns The response from the API
*/
public async deleteComment(commentId: string): Promise<any> {
try {
const url = `/comment/${commentId}`;
const response = await this.axiosInstance.delete(url);
return response.data;
} catch (error) {
console.error('Error deleting comment:', error);
throw error;
}
}
// Threaded Comments
/**
* Get threaded comments for a parent comment.
* @param commentId - ID of the parent comment
* @returns The response from the API
*/
public async getThreadedComments(commentId: string): Promise<any> {
try {
const url = `/comment/${commentId}/reply`;
const response = await this.axiosInstance.get(url);
return response.data;
} catch (error) {
console.error('Error getting threaded comments:', error);
throw error;
}
}
/**
* Create a threaded comment as a reply to another comment.
* @param commentId - ID of the parent comment
* @param commentText - Content of the threaded comment
* @param assignee - User ID to assign the comment to (optional)
* @param groupAssignee - Group to assign the comment to (optional)
* @param notifyAll - If true, notifications will be sent to everyone including the comment creator
* @returns The response from the API
*/
public async createThreadedComment(
commentId: string,
commentText: string,
assignee?: number,
groupAssignee?: string,
notifyAll: boolean = false
): Promise<any> {
try {
const url = `/comment/${commentId}/reply`;
const body: any = {
comment_text: commentText,
notify_all: notifyAll
};
if (assignee) {
body.assignee = assignee;
}
if (groupAssignee) {
body.group_assignee = groupAssignee;
}
const response = await this.axiosInstance.post(url, body);
return response.data;
} catch (error) {
console.error('Error creating threaded comment:', error);
throw error;
}
}
}