Skip to main content
Glama

ClickUp MCP

by TwoFeetUp
container-handlers.ts12.3 kB
/** * SPDX-FileCopyrightText: © 2025 Sjoerd Tiemensma * SPDX-License-Identifier: MIT * * ClickUp MCP Container Handlers * * Handlers for consolidated container (list/folder) management tools. * Routes to existing list.ts and folder.ts handlers while providing * unified parameter handling, response formatting, and caching. */ import { Logger } from '../logger.js'; import { formatResponse, formatError, DetailLevel } from '../utils/response-formatter.js'; import { workspaceCache, cacheService } from '../utils/cache-service.js'; import { sponsorService } from '../utils/sponsor-service.js'; import config from '../config.js'; // Import handlers from existing modules import { handleCreateList, handleCreateListInFolder, handleGetList, handleUpdateList, handleDeleteList, findListIDByName } from './list.js'; import { handleCreateFolder, handleGetFolder, handleUpdateFolder, handleDeleteFolder } from './folder.js'; import { clickUpServices } from '../services/shared.js'; const logger = new Logger('ContainerHandlers'); // Cache keys const CACHE_KEYS = { LIST: (id: string) => `container:list:${id}`, FOLDER: (id: string) => `container:folder:${id}`, LIST_BY_NAME: (name: string) => `container:list:name:${name}`, FOLDER_BY_NAME: (name: string) => `container:folder:name:${name}` }; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Resolve a container ID from flexible identification options */ async function resolveContainerId( type: 'list' | 'folder', id?: string, name?: string, spaceId?: string, spaceName?: string, folderId?: string, folderName?: string ): Promise<string> { // If ID is provided, use it directly if (id) { return id; } if (!name) { throw new Error(`Either ID or name must be provided to identify ${type}`); } const { workspace: workspaceService } = clickUpServices; try { if (type === 'list') { // For lists, try to find by name in hierarchy const listResult = await findListIDByName(workspaceService, name); if (listResult) { return listResult.id; } // If not found globally, try within specific space if (spaceId || spaceName) { let targetSpaceId = spaceId; if (!targetSpaceId && spaceName) { const spaceResult = await workspaceService.findSpaceByName(spaceName); if (!spaceResult) { throw new Error(`Space "${spaceName}" not found`); } targetSpaceId = spaceResult.id; } // Optimization: Use direct API call when we have spaceId instead of fetching full hierarchy const listsInSpace = await workspaceService.getListsInSpace(targetSpaceId); const matchingList = listsInSpace.find((list: any) => list.name === name); if (matchingList) { logger.debug(`Found list "${name}" in space ${targetSpaceId} via direct API call`); return matchingList.id; } } throw new Error(`List "${name}" not found`); } else { // For folders, resolve space first if needed let targetSpaceId = spaceId; if (!targetSpaceId && spaceName) { const spaceResult = await workspaceService.findSpaceByName(spaceName); if (!spaceResult) { throw new Error(`Space "${spaceName}" not found`); } targetSpaceId = spaceResult.id; } if (!targetSpaceId) { throw new Error("Space ID or name required when identifying folder by name"); } const { folder: folderService } = clickUpServices; const folderResult = await folderService.findFolderByName(targetSpaceId, name); if (!folderResult) { throw new Error(`Folder "${name}" not found in space`); } return folderResult.id; } } catch (error: any) { logger.error(`Failed to resolve ${type} ID from name`, { name, error: error.message }); throw error; } } /** * Format container response with detail level and field selection */ function formatContainerResponse( data: any, detailLevel: DetailLevel = 'standard', fields?: string[] ) { // Define field mappings for lists and folders const containerFields = { list: { minimal: ['id', 'name'], standard: ['id', 'name', 'space', 'folder', 'archived', 'url'], detailed: ['*'] // All fields }, folder: { minimal: ['id', 'name'], standard: ['id', 'name', 'space', 'archived'], detailed: ['*'] } }; // Apply formatting return formatResponse(data, { detailLevel, fields, includeMetadata: true }); } /** * Handler for manage_container tool */ export async function handleManageContainer(parameters: any) { const { type, action, id, name, newName, spaceId, spaceName, folderId, folderName, content, dueDate, priority, assignee, status, override_statuses, detail_level = 'standard', fields } = parameters; logger.info(`Managing container: type=${type}, action=${action}`); try { // Validate input if (!type || !['list', 'folder'].includes(type)) { throw new Error("Invalid container type. Must be 'list' or 'folder'"); } if (!action || !['create', 'update', 'delete'].includes(action)) { throw new Error("Invalid action. Must be 'create', 'update', or 'delete'"); } // Route to appropriate handler based on type and action if (type === 'list') { return await handleListContainer( action, { id, name, newName, spaceId, spaceName, folderId, folderName, content, dueDate, priority, assignee, status }, detail_level, fields ); } else { return await handleFolderContainer( action, { id, name, newName, spaceId, spaceName, override_statuses }, detail_level, fields ); } } catch (error: any) { logger.error('Failed to manage container', { error: error.message }); return sponsorService.createErrorResponse( `Failed to manage ${type}: ${error.message}` ); } } /** * Handle list container operations */ async function handleListContainer( action: string, params: any, detailLevel: DetailLevel, fields?: string[] ) { const { id, name, newName, spaceId, spaceName, folderId, folderName, content, dueDate, priority, assignee, status } = params; try { switch (action) { case 'create': { // Determine if creating in space or folder if (folderId || folderName) { // Create in folder const result = await handleCreateListInFolder({ name, folderId, folderName, spaceId, spaceName, content, status }); return sponsorService.createResponse(result); } else { // Create in space const result = await handleCreateList({ name, spaceId, spaceName, content, dueDate, priority, assignee, status }); return sponsorService.createResponse(result); } } case 'update': { // Resolve list ID const listId = await resolveContainerId('list', id, name, spaceId, spaceName, folderId, folderName); // Prepare update data const updateParams: any = { listId }; if (newName) updateParams.name = newName; if (content) updateParams.content = content; if (status) updateParams.status = status; const result = await handleUpdateList(updateParams); return sponsorService.createResponse(result); } case 'delete': { // Resolve list ID const listId = await resolveContainerId('list', id, name, spaceId, spaceName, folderId, folderName); const result = await handleDeleteList({ listId }); return sponsorService.createResponse(result); } default: throw new Error(`Unknown action: ${action}`); } } catch (error: any) { logger.error('Failed to handle list container', { action, error: error.message }); throw error; } } /** * Handle folder container operations */ async function handleFolderContainer( action: string, params: any, detailLevel: DetailLevel, fields?: string[] ) { const { id, name, newName, spaceId, spaceName, override_statuses } = params; try { switch (action) { case 'create': { if (!name) { throw new Error("Folder name is required for create action"); } const result = await handleCreateFolder({ name, spaceId, spaceName, override_statuses }); return sponsorService.createResponse(result); } case 'update': { // Resolve folder ID const folderId = await resolveContainerId('folder', id, name, spaceId, spaceName); // Prepare update data const updateParams: any = { folderId }; if (newName) updateParams.name = newName; if (override_statuses !== undefined) updateParams.override_statuses = override_statuses; const result = await handleUpdateFolder(updateParams); return sponsorService.createResponse(result); } case 'delete': { // Resolve folder ID const folderId = await resolveContainerId('folder', id, name, spaceId, spaceName); const result = await handleDeleteFolder({ folderId }); return sponsorService.createResponse(result); } default: throw new Error(`Unknown action: ${action}`); } } catch (error: any) { logger.error('Failed to handle folder container', { action, error: error.message }); throw error; } } /** * Handler for get_container tool */ export async function handleGetContainer(parameters: any) { const { type, id, name, spaceId, spaceName, folderId, folderName, detail_level = 'standard', fields, use_cache = true } = parameters; logger.info(`Retrieving container: type=${type}`); try { // Validate input if (!type || !['list', 'folder'].includes(type)) { throw new Error("Invalid container type. Must be 'list' or 'folder'"); } // Check cache first if enabled let cacheKey: string | null = null; if (use_cache && id) { cacheKey = type === 'list' ? CACHE_KEYS.LIST(id) : CACHE_KEYS.FOLDER(id); const cached = cacheService.get(cacheKey); if (cached) { logger.debug('Cache hit for container', { type, id }); return sponsorService.createResponse( formatContainerResponse(cached, detail_level as DetailLevel, fields).data ); } } // Resolve container ID const containerId = await resolveContainerId( type, id, name, spaceId, spaceName, folderId, folderName ); // Fetch container details let result: any; if (type === 'list') { result = await handleGetList({ listId: containerId }); } else { result = await handleGetFolder({ folderId: containerId }); } // Cache the result if (use_cache && cacheKey && result && typeof result === 'object' && result.data) { cacheService.set(cacheKey, result.data, CACHE_TTL); } return result; } catch (error: any) { logger.error('Failed to retrieve container', { type, error: error.message }); return sponsorService.createErrorResponse( `Failed to retrieve ${type}: ${error.message}` ); } } /** * Invalidate container cache when containers are modified */ export function invalidateContainerCache(type: 'list' | 'folder', id: string) { const cacheKey = type === 'list' ? CACHE_KEYS.LIST(id) : CACHE_KEYS.FOLDER(id); cacheService.delete(cacheKey); logger.debug('Container cache invalidated', { type, id }); } /** * Clear all container caches */ export function clearContainerCaches() { cacheService.clearPattern(/^container:/); logger.info('All container caches cleared'); }

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