import axios, { AxiosInstance } from "axios";
import { loadConfig } from "./auth.js";
export class MCP {
private client: AxiosInstance;
constructor() {
const config = loadConfig();
if (!config.accessToken) {
throw new Error("Access token not found. Please run 'login' first.");
}
this.client = axios.create({
baseURL: "https://api.clickup.com/api/v2",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.accessToken}`,
},
});
}
private async request(method: string, url: string, data?: any, params?: any) {
try {
const response = await this.client.request({
method,
url,
data,
params,
});
return response.data;
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
console.error("ClickUp API Error:", JSON.stringify(error.response.data, null, 2));
throw new Error(error.response.data.err || JSON.stringify(error.response.data));
}
throw error;
}
}
public async getTasks(listId: string): Promise<any> {
const data = await this.request("GET", `/list/${listId}/task`);
return data.tasks;
}
public async createTask(listId: string, name: string, description?: string): Promise<any> {
return this.request("POST", `/list/${listId}/task`, { name, description });
}
public async updateTask(taskId: string, data: any): Promise<any> {
return this.request("PUT", `/task/${taskId}`, data);
}
public async getTask(taskId: string): Promise<any> {
return this.request("GET", `/task/${taskId}`);
}
public async getComments(taskId: string): Promise<any> {
const data = await this.request("GET", `/task/${taskId}/comment`);
return data.comments;
}
public async createComment(taskId: string, comment_text: string): Promise<any> {
return this.request("POST", `/task/${taskId}/comment`, { comment_text });
}
// Workspaces in ClickUp are called "Teams" in the API V2
public async getWorkspaces(): Promise<any> {
const data = await this.request("GET", "/team");
return data.teams;
}
public async getSpaces(workspaceId: string): Promise<any> {
const data = await this.request("GET", `/team/${workspaceId}/space`);
return data.spaces;
}
public async getFolders(spaceId: string): Promise<any> {
const data = await this.request("GET", `/space/${spaceId}/folder`);
return data.folders;
}
public async getLists(folderId: string): Promise<any> {
const data = await this.request("GET", `/folder/${folderId}/list`);
return data.lists;
}
public async getSpaceLists(spaceId: string): Promise<any> {
const data = await this.request("GET", `/space/${spaceId}/list`);
return data.lists;
}
public async getList(listId: string): Promise<any> {
return this.request("GET", `/list/${listId}`);
}
public async getAccessibleCustomFields(listId: string): Promise<any> {
const data = await this.request("GET", `/list/${listId}/field`);
return data.fields;
}
public async createCustomField(listId: string, name: string, type: string, type_config?: any): Promise<any> {
return this.request("POST", `/list/${listId}/field`, { name, type, type_config });
}
public async getCurrentUser(): Promise<any> {
const data = await this.request("GET", "/user");
return data.user;
}
// Space Management
public async createSpace(workspaceId: string, name: string): Promise<any> {
return this.request("POST", `/team/${workspaceId}/space`, { name });
}
public async updateSpace(spaceId: string, name: string): Promise<any> {
return this.request("PUT", `/space/${spaceId}`, { name });
}
public async deleteSpace(spaceId: string): Promise<any> {
return this.request("DELETE", `/space/${spaceId}`);
}
// Folder Management
public async createFolder(spaceId: string, name: string): Promise<any> {
return this.request("POST", `/space/${spaceId}/folder`, { name });
}
public async updateFolder(folderId: string, name: string): Promise<any> {
return this.request("PUT", `/folder/${folderId}`, { name });
}
public async deleteFolder(folderId: string): Promise<any> {
return this.request("DELETE", `/folder/${folderId}`);
}
// List Management
public async createListInFolder(folderId: string, name: string): Promise<any> {
return this.request("POST", `/folder/${folderId}/list`, { name });
}
public async createListInSpace(spaceId: string, name: string): Promise<any> {
return this.request("POST", `/space/${spaceId}/list`, { name });
}
public async updateList(listId: string, name: string): Promise<any> {
return this.request("PUT", `/list/${listId}`, { name });
}
public async deleteList(listId: string): Promise<any> {
return this.request("DELETE", `/list/${listId}`);
}
// Global Discovery Methods
public async findListByName(workspaceId: string, name: string): Promise<any[]> {
const spaces = await this.getSpaces(workspaceId);
const matches: any[] = [];
for (const space of spaces) {
// Check space-level lists
const spaceLists = await this.getSpaceLists(space.id);
matches.push(...spaceLists.filter((l: any) => l.name.toLowerCase().includes(name.toLowerCase())));
// Check folder-level lists
const folders = await this.getFolders(space.id);
for (const folder of folders) {
const folderLists = await this.getLists(folder.id);
matches.push(...folderLists.filter((l: any) => l.name.toLowerCase().includes(name.toLowerCase())));
}
}
return matches;
}
// Tag Management
public async getTags(spaceId: string): Promise<any> {
const data = await this.request("GET", `/space/${spaceId}/tag`);
return data.tags;
}
public async createTag(spaceId: string, name: string, tag_bg?: string, tag_fg?: string): Promise<any> {
const body: any = { tag: { name } };
if (tag_bg) body.tag.tag_bg = tag_bg;
if (tag_fg) body.tag.tag_fg = tag_fg;
return this.request("POST", `/space/${spaceId}/tag`, body);
}
public async editTag(spaceId: string, tagName: string, newName: string, tag_bg?: string, tag_fg?: string): Promise<any> {
const body: any = { tag: { name: newName } };
if (tag_bg) body.tag.tag_bg = tag_bg;
if (tag_fg) body.tag.tag_fg = tag_fg;
return this.request("PUT", `/space/${spaceId}/tag/${tagName}`, body);
}
public async deleteTag(spaceId: string, tagName: string): Promise<any> {
return this.request("DELETE", `/space/${spaceId}/tag/${tagName}`);
}
public async addTagToTask(taskId: string, tagName: string): Promise<any> {
return this.request("POST", `/task/${taskId}/tag/${tagName}`);
}
public async removeTagFromTask(taskId: string, tagName: string): Promise<any> {
return this.request("DELETE", `/task/${taskId}/tag/${tagName}`);
}
// Member & User Group Management
public async getWorkspaceMembers(workspaceId: string): Promise<any> {
const data = await this.request("GET", "/team");
const team = data.teams.find((t: any) => t.id === workspaceId);
if (!team) throw new Error(`Workspace with ID ${workspaceId} not found`);
return team.members;
}
public async getUserGroups(workspaceId: string): Promise<any> {
const data = await this.request("GET", `/team/${workspaceId}/group`);
return data.groups;
}
public async createUserGroup(workspaceId: string, name: string, memberIds: string[] = []): Promise<any> {
// API expects numeric IDs for members
const body = { name, members: memberIds.map(id => parseInt(id)) };
return this.request("POST", `/team/${workspaceId}/group`, body);
}
public async updateUserGroup(groupId: string, name: string, memberIds: string[] = []): Promise<any> {
const body = { name, members: memberIds.map(id => parseInt(id)) };
return this.request("PUT", `/group/${groupId}`, body);
}
public async deleteUserGroup(groupId: string): Promise<any> {
return this.request("DELETE", `/group/${groupId}`);
}
// Time Tracking
// Requires Team ID. We'll try to fetch the first team if not provided, or require it?
// For simplicity, let's look up the user's first team if we need a team ID and don't have one,
// but startTimer/getTimeEntries via API usually works on Task or Team.
// API: GET /team/{team_id}/time_entries
public async getTimeEntries(taskId?: string): Promise<any> {
// We need a team ID due to API design. Let's fetch workspaces (teams) first.
const teams = await this.getWorkspaces();
if (!teams || teams.length === 0) throw new Error("No workspaces found.");
const teamId = teams[0].id;
const params: any = {};
if (taskId) params.task_id = taskId;
const data = await this.request("GET", `/team/${teamId}/time_entries`, undefined, params);
return data.data;
}
public async startTimer(taskId: string): Promise<any> {
const teams = await this.getWorkspaces();
if (!teams || teams.length === 0) throw new Error("No workspaces found.");
const teamId = teams[0].id;
return this.request("POST", `/team/${teamId}/time_entries/start`, {
task_id: taskId
});
}
public async stopTimer(): Promise<any> {
const teams = await this.getWorkspaces();
if (!teams || teams.length === 0) throw new Error("No workspaces found.");
const teamId = teams[0].id;
return this.request("POST", `/team/${teamId}/time_entries/stop`);
}
// Simplified Search (Task search only for now)
public async search(query: string, workspaceId: string): Promise<any> {
const params: any = {
include_closed: true,
subtasks: true,
search: query
};
const data = await this.request("GET", `/team/${workspaceId}/task`, undefined, params);
return data.tasks;
}
}