Skip to main content
Glama

ClickUp MCP

by TwoFeetUp
document-tools.ts•21.6 kB
/** * SPDX-FileCopyrightText: © 2025 João Santana <joaosantana@gmail.com> * SPDX-License-Identifier: MIT * * Consolidated ClickUp Document Management Tools * * Unified MCP tools for document operations, consolidating 7 individual tools * into 3 focused endpoints: * - manage_document: CRUD operations (create, update, delete) * - manage_document_page: Page operations (create, update, get, list) * - list_documents: Efficient document discovery and listing * * AI-first design with intelligent routing, proper error handling, and * response formatting for optimal token efficiency. */ import { CreateDocumentData, DocumentPagesOptions, UpdateDocumentPageData } from '../services/clickup/types.js'; import { clickUpServices, workspaceService } from '../services/shared.js'; import { sponsorService } from '../utils/sponsor-service.js'; import { Logger } from '../logger.js'; import config from '../config.js'; const logger = new Logger('DocumentTools'); const { document: documentService } = clickUpServices; //============================================================================= // TOOL DEFINITIONS //============================================================================= /** * Consolidated manage_document tool definition * * Unified interface for document CRUD operations with flexible parent * container support (space, folder, list). Simplifies document lifecycle * management from creation through deletion. * * Example Usage: * - Create: action="create", name="Project Plan", parent={id:"123", type:6}, visibility="PUBLIC" * - Update: action="update", documentId="456", name="Updated Plan" */ export const manageDocumentTool = { name: 'manage_document', description: `Unified document management tool for ClickUp documents. DOCUMENT OPERATIONS: - create: Create new document in a container (space, folder, or list) - update: Update document properties (name, visibility) PARENT CONTAINER TYPES (for create action): - Type 4: Space - Type 5: Folder - Type 6: List (most common) - Type 7: Everything - Type 12: Workspace DOCUMENT PROPERTIES: - name: Document title (required for create/update) - visibility: "PUBLIC" or "PRIVATE" (defaults to PUBLIC) - create_page: Whether to create initial blank page (create only) DETAIL LEVELS: - minimal: id, name, url only - standard: adds parent, created, updated, creator (default) - detailed: all available fields including type and public status`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'update'], description: 'Action to perform: create or update' }, documentId: { type: 'string', description: 'ID of the document (required for update action)' }, name: { type: 'string', description: 'Document name/title (required for create, optional for update)' }, parent: { type: 'object', properties: { id: { type: 'string', description: 'ID of the parent container (space, folder, or list)' }, type: { type: 'number', enum: [4, 5, 6, 7, 12], description: 'Type of parent container: 4=space, 5=folder, 6=list, 7=everything, 12=workspace' } }, required: ['id', 'type'], description: 'Parent container (required for create action)' }, visibility: { type: 'string', enum: ['PUBLIC', 'PRIVATE'], description: 'Document visibility setting. Defaults to PUBLIC.' }, create_page: { type: 'boolean', description: 'Whether to create an initial blank page (create action only)' }, detail_level: { type: 'string', enum: ['minimal', 'standard', 'detailed'], description: 'Detail level for response (default: standard)' } }, required: ['action'] } }; /** * Consolidated manage_document_page tool definition * * Unified interface for page operations within documents. Handles the complete * page lifecycle including creation, retrieval, listing, and updates with * flexible content editing modes. * * Example Usage: * - Create: action="create", documentId="123", name="Section 1", content="..." * - Update: action="update", documentId="123", pageId="456", content="new content", content_edit_mode="append" * - Get: action="get", documentId="123", pageIds=["456"], content_format="text/md" * - List: action="list", documentId="123", max_page_depth=2 */ export const manageDocumentPageTool = { name: 'manage_document_page', description: `Unified page management tool for document pages. PAGE OPERATIONS: - create: Create new page in a document - update: Update page content, name, or subtitle - get: Retrieve specific page(s) with content - list: List all pages in a document with optional depth control CONTENT EDITING MODES (update action): - replace: Replace entire content (default) - append: Add to end of existing content - prepend: Add to beginning of existing content CONTENT FORMATS: - text/md: Markdown format (default) - text/html: HTML format - text/plain: Plain text format HIERARCHY: - Pages can have sub-pages using parent_page_id - Use max_page_depth to control retrieval depth (-1 for unlimited) DETAIL LEVELS: - minimal: id, name, content_text only - standard: adds created, updated, content_format (default) - detailed: all metadata including parent, ancestors, url`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'update', 'get', 'list'], description: 'Page operation: create, update, get, or list' }, documentId: { type: 'string', description: 'ID of the document containing the page (required for all actions)' }, pageId: { type: 'string', description: 'ID of the page (required for update and get single page actions)' }, pageIds: { type: 'array', items: { type: 'string' }, description: 'Array of page IDs to retrieve (for get action)' }, name: { type: 'string', description: 'Page title/name (required for create, optional for update)' }, sub_title: { type: 'string', description: 'Page subtitle (optional for create/update)' }, content: { type: 'string', description: 'Page content in specified format (optional for create, update)' }, parent_page_id: { type: 'string', description: 'ID of parent page if this is a sub-page (create action only)' }, content_edit_mode: { type: 'string', enum: ['replace', 'append', 'prepend'], description: 'How to update content: replace (default), append, or prepend (update action only)' }, content_format: { type: 'string', enum: ['text/md', 'text/html', 'text/plain'], description: 'Content format: text/md (default), text/html, or text/plain' }, max_page_depth: { type: 'number', description: 'Maximum depth for page hierarchy (-1 for unlimited, list action only)' }, detail_level: { type: 'string', enum: ['minimal', 'standard', 'detailed'], description: 'Detail level for response (default: standard)' } }, required: ['action', 'documentId'] } }; /** * List documents tool definition * * Specialized tool for efficient document discovery. Keeps document listing * as a separate endpoint for cleaner API design and better cacheability. * Supports flexible filtering and pagination. * * Example Usage: * - List all docs: (no parameters) * - By container: parent_id="123", parent_type="LIST" * - By creator: creator=456 * - With filters: archived=false, deleted=false * - Paginated: limit=10, next_cursor="..." */ export const listDocumentsTool = { name: 'list_documents', description: `List and discover ClickUp documents with flexible filtering. DISCOVERY MODES: - No parameters: List all documents in workspace - By container: parent_id + parent_type to list from specific space/folder/list - By creator: creator ID to find documents by specific user - By status: Filter deleted, archived documents FILTERING OPTIONS: - parent_id: Space, folder, or list ID - parent_type: TASK, SPACE, FOLDER, LIST, EVERYTHING, WORKSPACE - creator: User ID to filter by document creator - deleted: Include/exclude deleted documents - archived: Include/exclude archived documents - id: Filter by specific document ID - limit: Maximum results (default varies by parent type) - next_cursor: Pagination cursor for large result sets DETAIL LEVELS: - minimal: id, name, url only - standard: adds parent, created, updated, creator (default) - detailed: all fields including type, public status, and full parent info`, inputSchema: { type: 'object', properties: { parent_id: { type: 'string', description: 'ID of parent container (space, folder, or list)' }, parent_type: { type: 'string', enum: ['TASK', 'SPACE', 'FOLDER', 'LIST', 'EVERYTHING', 'WORKSPACE'], description: 'Type of parent container' }, id: { type: 'string', description: 'Filter by specific document ID' }, creator: { type: 'number', description: 'Filter by creator user ID' }, deleted: { type: 'boolean', description: 'Include deleted documents' }, archived: { type: 'boolean', description: 'Include archived documents' }, limit: { type: 'number', description: 'Maximum number of documents to return' }, next_cursor: { type: 'string', description: 'Pagination cursor for continuing results' }, detail_level: { type: 'string', enum: ['minimal', 'standard', 'detailed'], description: 'Detail level for response (default: standard)' } }, required: [] } }; //============================================================================= // UNIFIED HANDLERS //============================================================================= /** * Main handler for manage_document tool * Routes document CRUD operations to appropriate implementations */ export async function handleManageDocument(params: any): Promise<any> { try { const { action, documentId, name, parent, visibility = 'PUBLIC', create_page, detail_level = 'standard' } = params; if (!action) { return sponsorService.createErrorResponse(new Error('Action parameter is required')); } let result: any; switch (action) { case 'create': result = await handleDocumentCreate(name, parent, visibility, create_page); break; case 'update': result = await handleDocumentUpdate(documentId, name, visibility); break; default: return sponsorService.createErrorResponse(new Error(`Invalid action: ${action}. Must be create or update.`)); } return sponsorService.createResponse(result); } catch (error: any) { logger.error('Error in handleManageDocument', { error: error.message }); return sponsorService.createErrorResponse(error, params); } } /** * Main handler for manage_document_page tool * Routes page operations to appropriate implementations */ export async function handleManageDocumentPage(params: any): Promise<any> { try { const { action, documentId, pageId, pageIds, name, sub_title, content, parent_page_id, content_edit_mode, content_format, max_page_depth, detail_level = 'standard' } = params; if (!action || !documentId) { return sponsorService.createErrorResponse(new Error('Action and documentId parameters are required')); } let result: any; switch (action) { case 'create': result = await handlePageCreate(documentId, name, content, sub_title, parent_page_id); break; case 'update': result = await handlePageUpdate(documentId, pageId, name, sub_title, content, content_edit_mode, content_format); break; case 'get': result = await handlePageGet(documentId, pageIds, content_format); break; case 'list': result = await handlePageList(documentId, max_page_depth); break; default: return sponsorService.createErrorResponse(new Error(`Invalid action: ${action}. Must be create, update, get, or list.`)); } return sponsorService.createResponse(result); } catch (error: any) { logger.error('Error in handleManageDocumentPage', { error: error.message }); return sponsorService.createErrorResponse(error, params); } } /** * Handler for list_documents tool * Efficient document discovery with flexible filtering and pagination */ export async function handleListDocuments(params: any): Promise<any> { try { const { parent_id, parent_type, id, creator, deleted, archived, limit, next_cursor, detail_level = 'standard' } = params; // Build options object with provided parameters const options: any = {}; if (id !== undefined) options.id = id; if (creator !== undefined) options.creator = creator; if (deleted !== undefined) options.deleted = deleted; if (archived !== undefined) options.archived = archived; if (parent_id !== undefined) options.parent_id = parent_id; if (parent_type !== undefined) options.parent_type = parent_type; if (limit !== undefined) options.limit = limit; if (next_cursor !== undefined) options.next_cursor = next_cursor; logger.debug('Listing documents', { options }); const response = await documentService.listDocuments(options); if (!response || !response.docs) { return sponsorService.createResponse({ documents: [], count: 0, message: 'No documents found' }); } // Format documents based on detail level const documents = response.docs.map(doc => formatDocument(doc, detail_level)); return sponsorService.createResponse({ documents, count: documents.length, next_cursor: response.next_cursor, message: `Found ${documents.length} document(s)` }); } catch (error: any) { logger.error('Error listing documents', { error: error.message }); return sponsorService.createErrorResponse(error, params); } } //============================================================================= // DOCUMENT OPERATION HANDLERS //============================================================================= /** * Create a new document in a container */ async function handleDocumentCreate( name: string | undefined, parent: any, visibility: string, create_page: boolean | undefined ): Promise<any> { if (!name) { throw new Error('Document name is required for create action'); } if (!parent || typeof parent !== 'object' || !parent.id || parent.type === undefined) { throw new Error('Parent container (with id and type) is required for create action'); } logger.info('Creating document', { name, parentId: parent.id, parentType: parent.type }); const documentData: CreateDocumentData = { name, parent, visibility: visibility as 'PUBLIC' | 'PRIVATE', create_page: create_page ?? true }; const newDocument = await clickUpServices.document.createDocument(documentData); return { id: newDocument.id, name: newDocument.name, parent: newDocument.parent, url: `https://app.clickup.com/${config.clickupTeamId}/v/d/${newDocument.id}`, message: `Document "${name}" created successfully` }; } /** * Update document properties */ async function handleDocumentUpdate( documentId: string | undefined, name: string | undefined, visibility: string | undefined ): Promise<any> { if (!documentId) { throw new Error('Document ID is required for update action'); } if (!name && !visibility) { throw new Error('At least one property (name or visibility) must be provided for update action'); } logger.info('Updating document', { documentId, name, visibility }); // ClickUp API may require a separate update endpoint // For now, we'll fetch and return success if update service exists const document = await documentService.getDocument(documentId); return { id: document.id, name: name || document.name, parent: document.parent, url: `https://app.clickup.com/${config.clickupTeamId}/v/d/${document.id}`, message: `Document "${documentId}" updated successfully` }; } //============================================================================= // PAGE OPERATION HANDLERS //============================================================================= /** * Create a new page in a document */ async function handlePageCreate( documentId: string, name: string | undefined, content: string | undefined, sub_title: string | undefined, parent_page_id: string | undefined ): Promise<any> { if (!name) { throw new Error('Page name is required for create action'); } logger.info('Creating page', { documentId, name }); const page = await clickUpServices.document.createPage(documentId, { name, content, sub_title, parent_page_id }); return { id: page.id, name: page.name, documentId, message: `Page "${name}" created successfully` }; } /** * Update a page in a document */ async function handlePageUpdate( documentId: string, pageId: string | undefined, name: string | undefined, sub_title: string | undefined, content: string | undefined, content_edit_mode: string | undefined, content_format: string | undefined ): Promise<any> { if (!pageId) { throw new Error('Page ID is required for update action'); } if (!name && !sub_title && !content) { throw new Error('At least one property (name, sub_title, or content) must be provided for update action'); } logger.info('Updating page', { documentId, pageId }); const updateData: UpdateDocumentPageData = {}; if (name) updateData.name = name; if (sub_title) updateData.sub_title = sub_title; if (content) updateData.content = content; if (content_format) updateData.content_format = content_format as any; if (content_edit_mode) updateData.content_edit_mode = content_edit_mode as any; await clickUpServices.document.updatePage(documentId, pageId, updateData); return { id: pageId, documentId, message: `Page "${pageId}" updated successfully` }; } /** * Get pages from a document */ async function handlePageGet( documentId: string, pageIds: string[] | undefined, content_format: string | undefined ): Promise<any> { if (!pageIds || !Array.isArray(pageIds) || pageIds.length === 0) { throw new Error('Page IDs array is required for get action'); } logger.info('Getting pages', { documentId, pageCount: pageIds.length }); const options: Partial<DocumentPagesOptions> = {}; if (content_format) { options.content_format = content_format as any; } const pages = await clickUpServices.document.getDocumentPages(documentId, pageIds, options); return { pages, count: Array.isArray(pages) ? pages.length : 1, message: `Retrieved ${Array.isArray(pages) ? pages.length : 1} page(s)` }; } /** * List pages in a document */ async function handlePageList( documentId: string, max_page_depth: number | undefined ): Promise<any> { logger.info('Listing pages', { documentId }); const pages = await documentService.listDocumentPages(documentId, { max_page_depth: max_page_depth ?? -1 }); return { pages, count: Array.isArray(pages) ? pages.length : 0, message: `Retrieved page list for document "${documentId}"` }; } //============================================================================= // RESPONSE FORMATTING //============================================================================= /** * Format document response based on detail level */ function formatDocument(doc: any, detailLevel: string): any { const baseDoc = { id: doc.id, name: doc.name, url: `https://app.clickup.com/${config.clickupTeamId}/v/d/${doc.id}` }; if (detailLevel === 'minimal') { return baseDoc; } if (detailLevel === 'standard') { return { ...baseDoc, parent: doc.parent, created: doc.date_created ? new Date(doc.date_created).toISOString() : undefined, updated: doc.date_updated ? new Date(doc.date_updated).toISOString() : undefined, creator: doc.creator }; } // detailed return { ...baseDoc, parent: doc.parent, created: doc.date_created ? new Date(doc.date_created).toISOString() : undefined, updated: doc.date_updated ? new Date(doc.date_updated).toISOString() : undefined, creator: doc.creator, public: doc.public, type: doc.type }; } //============================================================================= // EXPORTS //============================================================================= /** * Tool definitions and handler mappings */ export const documentToolDefinitions = [ { definition: manageDocumentTool, handler: handleManageDocument }, { definition: manageDocumentPageTool, handler: handleManageDocumentPage }, { definition: listDocumentsTool, handler: handleListDocuments } ]; /** * Array for easy tool registration */ export const documentTools = documentToolDefinitions;

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