ClickUp MCP Server
by windalfin
Verified
/**
* ClickUp Workspace Service Module
*
* Handles workspace hierarchy and space-related operations
*/
import { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
import {
ClickUpSpace,
ClickUpFolder,
ClickUpList,
WorkspaceTree,
WorkspaceNode
} from './types.js';
/**
* Service for workspace-related operations
*/
export class WorkspaceService extends BaseClickUpService {
// Store the workspace hierarchy in memory
private workspaceHierarchy: WorkspaceTree | null = null;
/**
* Creates an instance of WorkspaceService
* @param apiKey - ClickUp API key
* @param teamId - ClickUp team ID
* @param baseUrl - Optional custom API URL
*/
constructor(
apiKey: string,
teamId: string,
baseUrl?: string
) {
super(apiKey, teamId, baseUrl);
}
/**
* Helper method to handle errors consistently
* @param error - Error caught from a try/catch
* @param message - Optional message to add to the error
* @returns - A standardized ClickUpServiceError
*/
private handleError(error: any, message?: string): ClickUpServiceError {
// Log the error for debugging
console.error('WorkspaceService error:', error);
// If the error is already a ClickUpServiceError, return it
if (error instanceof ClickUpServiceError) {
return error;
}
// Otherwise, create a new ClickUpServiceError
const errorMessage = message || 'An error occurred in WorkspaceService';
return new ClickUpServiceError(errorMessage, ErrorCode.WORKSPACE_ERROR, error);
}
/**
* Get all spaces for the team
* @returns - Promise resolving to array of spaces
*/
async getSpaces(): Promise<ClickUpSpace[]> {
try {
const response = await this.makeRequest(async () => {
const result = await this.client.get(`/team/${this.teamId}/space`);
return result.data;
});
return response.spaces || [];
} catch (error) {
throw this.handleError(error, 'Failed to get spaces');
}
}
/**
* Get a specific space by ID
* @param spaceId - The ID of the space to retrieve
* @returns - Promise resolving to the space object
*/
async getSpace(spaceId: string): Promise<ClickUpSpace> {
try {
// Validate spaceId
if (!spaceId) {
throw new ClickUpServiceError(
'Space ID is required',
ErrorCode.INVALID_PARAMETER
);
}
return await this.makeRequest(async () => {
const result = await this.client.get(`/space/${spaceId}`);
return result.data;
});
} catch (error) {
throw this.handleError(error, `Failed to get space with ID ${spaceId}`);
}
}
/**
* Find a space by name
* @param spaceName - The name of the space to find
* @returns - Promise resolving to the space or null if not found
*/
async findSpaceByName(spaceName: string): Promise<ClickUpSpace | null> {
try {
// Validate spaceName
if (!spaceName) {
throw new ClickUpServiceError(
'Space name is required',
ErrorCode.INVALID_PARAMETER
);
}
// Get all spaces and find the one with the matching name
const spaces = await this.getSpaces();
const space = spaces.find(s => s.name === spaceName);
return space || null;
} catch (error) {
throw this.handleError(error, `Failed to find space with name ${spaceName}`);
}
}
/**
* Get the complete workspace hierarchy including spaces, folders, and lists
* @param forceRefresh - Whether to force a refresh of the hierarchy
* @returns - Promise resolving to the workspace tree
*/
async getWorkspaceHierarchy(forceRefresh = false): Promise<WorkspaceTree> {
try {
// If we have the hierarchy in memory and not forcing refresh, return it
if (this.workspaceHierarchy && !forceRefresh) {
return this.workspaceHierarchy;
}
// Start building the workspace tree
const workspaceTree: WorkspaceTree = {
root: {
id: this.teamId,
name: 'Workspace',
children: []
}
};
// Get all spaces
const spaces = await this.getSpaces();
// Process each space
for (const space of spaces) {
const spaceNode: WorkspaceNode = {
id: space.id,
name: space.name,
type: 'space',
children: []
};
// Get folders for the space
const folders = await this.getFoldersInSpace(space.id);
for (const folder of folders) {
const folderNode: WorkspaceNode = {
id: folder.id,
name: folder.name,
type: 'folder',
parentId: space.id,
children: []
};
// Get lists in the folder
const listsInFolder = await this.getListsInFolder(folder.id);
for (const list of listsInFolder) {
folderNode.children?.push({
id: list.id,
name: list.name,
type: 'list',
parentId: folder.id
});
}
spaceNode.children?.push(folderNode);
}
// Get lists directly in the space (not in any folder)
const listsInSpace = await this.getListsInSpace(space.id);
for (const list of listsInSpace) {
spaceNode.children?.push({
id: list.id,
name: list.name,
type: 'list',
parentId: space.id
});
}
workspaceTree.root.children.push(spaceNode);
}
// Store the hierarchy for later use
this.workspaceHierarchy = workspaceTree;
return workspaceTree;
} catch (error) {
throw this.handleError(error, 'Failed to get workspace hierarchy');
}
}
/**
* Clear the stored workspace hierarchy, forcing a fresh fetch on next request
*/
clearWorkspaceHierarchy(): void {
this.workspaceHierarchy = null;
}
/**
* Find a node in the workspace tree by name and type
* @param node - The node to start searching from
* @param name - The name to search for
* @param type - The type of node to search for
* @returns - The node and its path if found, null otherwise
*/
private findNodeInTree(
node: WorkspaceNode | WorkspaceTree['root'],
name: string,
type: 'space' | 'folder' | 'list'
): { node: WorkspaceNode; path: string } | null {
// If this is the node we're looking for, return it
if ('type' in node && node.type === type && node.name === name) {
return { node, path: node.name };
}
// Otherwise, search its children recursively
for (const child of (node.children || [])) {
const result = this.findNodeInTree(child, name, type);
if (result) {
// Prepend this node's name to the path
const currentNodeName = 'name' in node ? node.name : 'Workspace';
result.path = `${currentNodeName} > ${result.path}`;
return result;
}
}
// Not found in this subtree
return null;
}
/**
* Find an ID by name and type in the workspace hierarchy
* @param hierarchy - The workspace hierarchy
* @param name - The name to search for
* @param type - The type of node to search for
* @returns - The ID and path if found, null otherwise
*/
findIDByNameInHierarchy(
hierarchy: WorkspaceTree,
name: string,
type: 'space' | 'folder' | 'list'
): { id: string; path: string } | null {
const result = this.findNodeInTree(hierarchy.root, name, type);
if (result) {
return { id: result.node.id, path: result.path };
}
return null;
}
/**
* Find a space ID by name
* @param spaceName - The name of the space to find
* @returns - Promise resolving to the space ID or null if not found
*/
async findSpaceIDByName(spaceName: string): Promise<string | null> {
const space = await this.findSpaceByName(spaceName);
return space ? space.id : null;
}
/**
* Get lists from the API (using the appropriate endpoint based on context)
* @param spaceId - The ID of the space
* @returns - Promise resolving to array of lists
*/
private async getLists(spaceId: string): Promise<any[]> {
try {
const response = await this.makeRequest(async () => {
const result = await this.client.get(`/space/${spaceId}/list`);
return result.data;
});
return response.lists || [];
} catch (error) {
throw this.handleError(error, `Failed to get lists for space ${spaceId}`);
}
}
/**
* Get folders from the API
* @param spaceId - The ID of the space
* @returns - Promise resolving to array of folders
*/
private async getFolders(spaceId: string): Promise<any[]> {
try {
const response = await this.makeRequest(async () => {
const result = await this.client.get(`/space/${spaceId}/folder`);
return result.data;
});
return response.folders || [];
} catch (error) {
throw this.handleError(error, `Failed to get folders for space ${spaceId}`);
}
}
/**
* Get a specific folder by ID
* @param folderId - The ID of the folder to retrieve
* @returns - Promise resolving to the folder
*/
async getFolder(folderId: string): Promise<any> {
try {
return await this.makeRequest(async () => {
const result = await this.client.get(`/folder/${folderId}`);
return result.data;
});
} catch (error) {
throw this.handleError(error, `Failed to get folder with ID ${folderId}`);
}
}
/**
* Get lists in a space (not in any folder)
* @param spaceId - The ID of the space
* @returns - Promise resolving to array of lists
*/
async getListsInSpace(spaceId: string): Promise<ClickUpList[]> {
try {
const lists = await this.getLists(spaceId);
return lists.filter((list) => !list.folder); // Filter lists not in a folder
} catch (error) {
throw this.handleError(error, `Failed to get lists in space ${spaceId}`);
}
}
/**
* Get folders in a space
* @param spaceId - The ID of the space
* @returns - Promise resolving to array of folders
*/
async getFoldersInSpace(spaceId: string): Promise<ClickUpFolder[]> {
try {
return await this.getFolders(spaceId);
} catch (error) {
throw this.handleError(error, `Failed to get folders in space ${spaceId}`);
}
}
/**
* Get lists in a folder
* @param folderId - The ID of the folder
* @returns - Promise resolving to array of lists
*/
async getListsInFolder(folderId: string): Promise<ClickUpList[]> {
try {
const response = await this.makeRequest(async () => {
const result = await this.client.get(`/folder/${folderId}/list`);
return result.data;
});
return response.lists || [];
} catch (error) {
throw this.handleError(error, `Failed to get lists in folder ${folderId}`);
}
}
}