Skip to main content
Glama
custom-field-tools.ts26.2 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { createClickUpClient } from '../clickup-client/index.js'; import { createEnhancedCustomFieldsClient, CustomFieldType } from '../clickup-client/custom-fields-enhanced.js'; import { /* CustomFieldToolSchemas, */ DropdownOptionSchema } from '../schemas/custom-field-schemas.js'; // Create clients const clickUpClient = createClickUpClient(); const customFieldsClient = createEnhancedCustomFieldsClient(clickUpClient); export function setupCustomFieldTools(server: McpServer): void { // ======================================== // GET CUSTOM FIELDS OPERATIONS // ======================================== server.tool( 'clickup_get_custom_fields', 'Get custom fields for a ClickUp list, folder, or space. Returns all field definitions with their configurations.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container to get custom fields from'), container_id: z.string().min(1).describe('The ID of the container (list, folder, or space)'), include_deleted: z.boolean().optional().default(false).describe('Whether to include deleted custom fields') }, async ({ container_type, container_id, include_deleted }) => { try { let fields; switch (container_type) { case 'list': fields = await customFieldsClient.getListCustomFields(container_id, { include_deleted }); break; case 'folder': fields = await customFieldsClient.getFolderCustomFields(container_id, { include_deleted }); break; case 'space': fields = await customFieldsClient.getSpaceCustomFields(container_id, { include_deleted }); break; default: throw new Error('Invalid container type'); } return { content: [{ type: 'text', text: `Custom fields for ${container_type} ${container_id}:\n\n${JSON.stringify(fields, null, 2)}` }] }; } catch (error: any) { console.error('Error getting custom fields:', error); return { content: [{ type: 'text', text: `Error getting custom fields: ${error.message}` }], isError: true }; } } ); // ======================================== // CREATE CUSTOM FIELD OPERATIONS // ======================================== server.tool( 'clickup_create_custom_field', 'Create a new custom field in a ClickUp list, folder, or space. Supports all field types with type-specific configurations.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container to create the custom field in'), container_id: z.string().min(1).describe('The ID of the container (list, folder, or space)'), name: z.string().min(1).max(255).describe('The name of the custom field'), type: z.enum([ 'text', 'textarea', 'number', 'currency', 'date', 'drop_down', 'labels', 'checkbox', 'url', 'email', 'phone', 'rating', 'progress', 'task_relationship' ] as const).describe('The type of custom field to create'), type_config: z.record(z.any()).optional().describe('Type-specific configuration object'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide the field from guests') }, async ({ container_type, container_id, name, type, type_config, required, hide_from_guests }) => { try { // Get default configuration for field type if not provided const finalTypeConfig = type_config || customFieldsClient.getFieldTypeTemplate(type as CustomFieldType); const fieldParams = { name, type: type as CustomFieldType, type_config: finalTypeConfig, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; default: throw new Error('Invalid container type'); } return { content: [{ type: 'text', text: `Custom field created successfully in ${container_type} ${container_id}!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating custom field:', error); return { content: [{ type: 'text', text: `Error creating custom field: ${error.message}` }], isError: true }; } } ); // ======================================== // UPDATE CUSTOM FIELD OPERATIONS // ======================================== server.tool( 'clickup_update_custom_field', 'Update an existing custom field. Can modify name, configuration, required status, and guest visibility.', { field_id: z.string().min(1).describe('The ID of the custom field to update'), name: z.string().min(1).max(255).optional().describe('New name for the custom field'), type_config: z.record(z.any()).optional().describe('Updated type-specific configuration'), required: z.boolean().optional().describe('Whether the field should be required'), hide_from_guests: z.boolean().optional().describe('Whether to hide the field from guests') }, async ({ field_id, name, type_config, required, hide_from_guests }) => { try { // Validate that at least one field is being updated if (name === undefined && type_config === undefined && required === undefined && hide_from_guests === undefined) { return { content: [{ type: 'text', text: 'Error: Must specify at least one field to update (name, type_config, required, or hide_from_guests)' }], isError: true }; } const updatedField = await customFieldsClient.updateCustomField(field_id, { name, type_config, required, hide_from_guests }); return { content: [{ type: 'text', text: `Custom field updated successfully!\n\n${JSON.stringify(updatedField, null, 2)}` }] }; } catch (error: any) { console.error('Error updating custom field:', error); return { content: [{ type: 'text', text: `Error updating custom field: ${error.message}` }], isError: true }; } } ); // ======================================== // DELETE CUSTOM FIELD OPERATIONS // ======================================== server.tool( 'clickup_delete_custom_field', 'Delete a custom field from ClickUp. This will remove the field and all its values from tasks. This action cannot be undone.', { field_id: z.string().min(1).describe('The ID of the custom field to delete') }, async ({ field_id }) => { try { await customFieldsClient.deleteCustomField(field_id); return { content: [{ type: 'text', text: `Custom field ${field_id} deleted successfully. All field values have been removed from tasks.` }] }; } catch (error: any) { console.error('Error deleting custom field:', error); return { content: [{ type: 'text', text: `Error deleting custom field: ${error.message}` }], isError: true }; } } ); // ======================================== // CUSTOM FIELD VALUE OPERATIONS // ======================================== server.tool( 'clickup_set_custom_field_value', 'Set a custom field value on a ClickUp task. The value format depends on the field type.', { task_id: z.string().min(1).describe('The ID of the task to set the custom field value on'), field_id: z.string().min(1).describe('The ID of the custom field'), value: z.any().describe('The value to set (format depends on field type)') }, async ({ task_id, field_id, value }) => { try { await customFieldsClient.setCustomFieldValue(task_id, field_id, value); return { content: [{ type: 'text', text: `Custom field value set successfully on task ${task_id} for field ${field_id}.` }] }; } catch (error: any) { console.error('Error setting custom field value:', error); return { content: [{ type: 'text', text: `Error setting custom field value: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_remove_custom_field_value', 'Remove a custom field value from a ClickUp task. This clears the field value but keeps the field definition.', { task_id: z.string().min(1).describe('The ID of the task to remove the custom field value from'), field_id: z.string().min(1).describe('The ID of the custom field to clear') }, async ({ task_id, field_id }) => { try { await customFieldsClient.removeCustomFieldValue(task_id, field_id); return { content: [{ type: 'text', text: `Custom field value removed successfully from task ${task_id} for field ${field_id}.` }] }; } catch (error: any) { console.error('Error removing custom field value:', error); return { content: [{ type: 'text', text: `Error removing custom field value: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_get_custom_field_value', 'Get a custom field value from a ClickUp task. Returns the current value and field information.', { task_id: z.string().min(1).describe('The ID of the task to get the custom field value from'), field_id: z.string().min(1).describe('The ID of the custom field to retrieve') }, async ({ task_id, field_id }) => { try { const value = await customFieldsClient.getCustomFieldValue(task_id, field_id); return { content: [{ type: 'text', text: `Custom field value for task ${task_id}, field ${field_id}:\n\n${JSON.stringify(value, null, 2)}` }] }; } catch (error: any) { console.error('Error getting custom field value:', error); return { content: [{ type: 'text', text: `Error getting custom field value: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_bulk_set_custom_field_values', 'Set multiple custom field values on a ClickUp task in a single operation. More efficient than setting values individually.', { task_id: z.string().min(1).describe('The ID of the task to set custom field values on'), field_values: z.array(z.object({ field_id: z.string().min(1).describe('The ID of the custom field'), value: z.any().describe('The value to set (format depends on field type)') })).min(1).describe('Array of field ID and value pairs to set') }, async ({ task_id, field_values }) => { try { // Ensure all field_values have the required properties const validatedFieldValues = field_values.map(fv => ({ field_id: fv.field_id, value: fv.value })); const results = await customFieldsClient.bulkSetCustomFieldValues(task_id, validatedFieldValues); return { content: [{ type: 'text', text: `Bulk custom field values set successfully on task ${task_id}!\n\nResults:\n${JSON.stringify(results, null, 2)}` }] }; } catch (error: any) { console.error('Error bulk setting custom field values:', error); return { content: [{ type: 'text', text: `Error bulk setting custom field values: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_get_task_custom_field_values', 'Get all custom field values for a ClickUp task. Returns all field values with their definitions.', { task_id: z.string().min(1).describe('The ID of the task to get custom field values from') }, async ({ task_id }) => { try { const values = await customFieldsClient.getTaskCustomFieldValues(task_id); return { content: [{ type: 'text', text: `All custom field values for task ${task_id}:\n\n${JSON.stringify(values, null, 2)}` }] }; } catch (error: any) { console.error('Error getting task custom field values:', error); return { content: [{ type: 'text', text: `Error getting task custom field values: ${error.message}` }], isError: true }; } } ); // ======================================== // HELPER TOOLS FOR FIELD CREATION // ======================================== server.tool( 'clickup_create_text_custom_field', 'Create a text custom field with optional default value and placeholder.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container'), container_id: z.string().min(1).describe('The ID of the container'), name: z.string().min(1).max(255).describe('The name of the text field'), default_value: z.string().optional().describe('Default text value'), placeholder: z.string().optional().describe('Placeholder text'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide from guests') }, async ({ container_type, container_id, name, default_value, placeholder, required, hide_from_guests }) => { try { const type_config = { default: default_value, placeholder }; const fieldParams = { name, type: 'text' as CustomFieldType, type_config, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; } return { content: [{ type: 'text', text: `Text custom field created successfully!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating text custom field:', error); return { content: [{ type: 'text', text: `Error creating text custom field: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_create_dropdown_custom_field', 'Create a dropdown custom field with specified options.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container'), container_id: z.string().min(1).describe('The ID of the container'), name: z.string().min(1).max(255).describe('The name of the dropdown field'), options: z.array(DropdownOptionSchema).min(1).describe('Array of dropdown options'), default_option_index: z.number().min(0).optional().describe('Index of default selected option'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide from guests') }, async ({ container_type, container_id, name, options, default_option_index, required, hide_from_guests }) => { try { const type_config = { options: options.map((option, index) => ({ ...option, orderindex: option.orderindex ?? index })), default: default_option_index }; const fieldParams = { name, type: 'drop_down' as CustomFieldType, type_config, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; } return { content: [{ type: 'text', text: `Dropdown custom field created successfully with ${options.length} options!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating dropdown custom field:', error); return { content: [{ type: 'text', text: `Error creating dropdown custom field: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_create_number_custom_field', 'Create a number custom field with optional precision and default value.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container'), container_id: z.string().min(1).describe('The ID of the container'), name: z.string().min(1).max(255).describe('The name of the number field'), default_value: z.number().optional().describe('Default numeric value'), precision: z.number().min(0).max(8).optional().default(0).describe('Number of decimal places (0-8)'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide from guests') }, async ({ container_type, container_id, name, default_value, precision, required, hide_from_guests }) => { try { const type_config = { default: default_value, precision }; const fieldParams = { name, type: 'number' as CustomFieldType, type_config, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; } return { content: [{ type: 'text', text: `Number custom field created successfully with ${precision} decimal places!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating number custom field:', error); return { content: [{ type: 'text', text: `Error creating number custom field: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_create_date_custom_field', 'Create a date custom field with optional time inclusion.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container'), container_id: z.string().min(1).describe('The ID of the container'), name: z.string().min(1).max(255).describe('The name of the date field'), include_time: z.boolean().optional().default(false).describe('Whether to include time in the date field'), default_value: z.number().positive().optional().describe('Default date value (Unix timestamp)'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide from guests') }, async ({ container_type, container_id, name, include_time, default_value, required, hide_from_guests }) => { try { const type_config = { include_time, default: default_value }; const fieldParams = { name, type: 'date' as CustomFieldType, type_config, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; } return { content: [{ type: 'text', text: `Date custom field created successfully${include_time ? ' with time support' : ''}!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating date custom field:', error); return { content: [{ type: 'text', text: `Error creating date custom field: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_create_checkbox_custom_field', 'Create a checkbox (boolean) custom field with optional default value.', { container_type: z.enum(['list', 'folder', 'space']).describe('The type of container'), container_id: z.string().min(1).describe('The ID of the container'), name: z.string().min(1).max(255).describe('The name of the checkbox field'), default_value: z.boolean().optional().default(false).describe('Default checkbox state'), required: z.boolean().optional().default(false).describe('Whether the field is required'), hide_from_guests: z.boolean().optional().default(false).describe('Whether to hide from guests') }, async ({ container_type, container_id, name, default_value, required, hide_from_guests }) => { try { const type_config = { default: default_value }; const fieldParams = { name, type: 'checkbox' as CustomFieldType, type_config, required, hide_from_guests }; let field; switch (container_type) { case 'list': field = await customFieldsClient.createListCustomField(container_id, fieldParams); break; case 'folder': field = await customFieldsClient.createFolderCustomField(container_id, fieldParams); break; case 'space': field = await customFieldsClient.createSpaceCustomField(container_id, fieldParams); break; } return { content: [{ type: 'text', text: `Checkbox custom field created successfully!\n\n${JSON.stringify(field, null, 2)}` }] }; } catch (error: any) { console.error('Error creating checkbox custom field:', error); return { content: [{ type: 'text', text: `Error creating checkbox custom field: ${error.message}` }], isError: true }; } } ); // ======================================== // FIELD VALUE VALIDATION HELPER // ======================================== server.tool( 'clickup_validate_custom_field_value', 'Validate a custom field value against its field type and configuration. Useful for checking values before setting them.', { field_id: z.string().min(1).describe('The ID of the custom field'), container_type: z.enum(['list', 'folder', 'space']).describe('The type of container the field belongs to'), container_id: z.string().min(1).describe('The ID of the container'), value: z.any().describe('The value to validate') }, async ({ field_id, container_type, container_id, value }) => { try { // Get the field definition first let fields; switch (container_type) { case 'list': fields = await customFieldsClient.getListCustomFields(container_id); break; case 'folder': fields = await customFieldsClient.getFolderCustomFields(container_id); break; case 'space': fields = await customFieldsClient.getSpaceCustomFields(container_id); break; } const field = fields.find(f => f.id === field_id); if (!field) { return { content: [{ type: 'text', text: `Error: Custom field ${field_id} not found in ${container_type} ${container_id}` }], isError: true }; } // Validate the value const isValid = customFieldsClient.validateFieldValue(field, value); return { content: [{ type: 'text', text: `Validation result for field "${field.name}" (${field.type}):\n\nValue: ${JSON.stringify(value)}\nValid: ${isValid}\n\nField Configuration:\n${JSON.stringify(field.type_config, null, 2)}` }] }; } catch (error: any) { console.error('Error validating custom field value:', error); return { content: [{ type: 'text', text: `Error validating custom field value: ${error.message}` }], isError: true }; } } ); }

Latest Blog Posts

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/Chykalophia/ClickUp-MCP-Server---Enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server