import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { CONFIG } from "../shared/config";
import { isTaskId } from "../shared/utils";
export function registerCustomFieldTools(server: McpServer) {
server.tool(
"listCustomFields",
"List all available custom fields for a specific list or the entire workspace (via team ID). Use this to find field IDs and types before updating.",
{
list_id: z.string().optional().describe("The ID of the list to get custom fields for. If omitted, fields for the workspace (team) will be fetched."),
},
async ({ list_id }) => {
try {
let url: string;
if (list_id) {
url = `https://api.clickup.com/api/v2/list/${list_id}/field`;
} else {
url = `https://api.clickup.com/api/v2/team/${CONFIG.teamId}/customfield`;
}
const response = await fetch(url, {
headers: { Authorization: CONFIG.apiKey },
});
if (!response.ok) {
throw new Error(`Error fetching custom fields: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const fields = data.fields || [];
const formattedFields = fields.map((f: any) => {
return `- **${f.name}** (ID: \`${f.id}\`, Type: \`${f.type}\`)\n config: ${JSON.stringify(f.type_config, null, 2)}`;
}).join('\n\n');
return {
content: [
{
type: "text",
text: `Found ${fields.length} custom fields:\n\n${formattedFields}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing custom fields: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
}
);
server.tool(
"getTaskCustomFields",
"Get all custom field values for a specific task.",
{
task_id: z
.string()
.min(6)
.max(9)
.refine(val => isTaskId(val), {
message: "Task ID must be 6-9 alphanumeric characters only"
})
.describe(
`The 6-9 character ID of the task`
),
},
async ({ task_id }) => {
try {
const response = await fetch(
`https://api.clickup.com/api/v2/task/${task_id}?include_custom_fields=true`,
{ headers: { Authorization: CONFIG.apiKey } }
);
if (!response.ok) {
throw new Error(`Error fetching task: ${response.status} ${response.statusText}`);
}
const task = await response.json();
const customFields = task.custom_fields || [];
const formattedFields = customFields.map((f: any) => {
let valueDisplay = f.value;
// Try to make values more readable
if (f.type === 'drop_down' && f.type_config?.options) {
const option = f.type_config.options.find((o: any) => o.orderindex === f.value);
if (option) valueDisplay = `${option.name} (orderindex: ${f.value})`;
} else if (typeof f.value === 'object') {
valueDisplay = JSON.stringify(f.value);
}
return `- **${f.name}** (ID: \`${f.id}\`, Type: \`${f.type}\`): ${valueDisplay !== undefined ? valueDisplay : '(empty)'}`;
}).join('\n');
return {
content: [
{
type: "text",
text: `Custom Fields for task ${task.name} (${task.id}):\n\n${formattedFields}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting custom fields for task: ${error instanceof Error ? error.message : 'Unknown error'}`,
}
]
}
}
}
);
server.tool(
"updateTaskCustomField",
"Update the value of a specific custom field on a task.\n\nNote for different field types:\n- **Text/Number/Email/Url/Phone**: Pass the value directly as a string or number.\n- **Checkbox**: Pass 'true' or 'false'.\n- **Date**: Pass user-friendly date strings (e.g. '2025-07-01'), they will be converted to Unix timestamp (milliseconds).\n- **Dropdown**: Pass the UUID of the option OR the orderindex (number) depending on configuration.\n- **Labels/Multi-Select**: Pass an array of UUIDs usually.\n- **People**: Pass an array of user IDs.\n\nIf you are unsure of the value format, use `getTaskCustomFields` to see existing values or `listCustomFields` to see the structure.",
{
task_id: z.string().min(6).max(9).describe("The 6-9 character task ID"),
field_id: z.string().describe("The UUID of the custom field to update"),
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.record(z.any())]).describe("The new value for the custom field. Check tool description for type-specific formats."),
},
async ({ task_id, field_id, value }) => {
try {
let finalValue = value;
// Special handling for Dates if passed as string but NOT a timestamp number
// ClickUp expects milliseconds
if (typeof value === 'string' && !/^\d+$/.test(value) && Date.parse(value)) {
finalValue = new Date(value).getTime();
}
const response = await fetch(
`https://api.clickup.com/api/v2/task/${task_id}/field/${field_id}`,
{
method: 'POST', // ClickUp V2 API usually uses POST for setting custom field value
headers: {
Authorization: CONFIG.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: finalValue })
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Error updating custom field: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
}
return {
content: [
{
type: "text",
text: `Successfully updated custom field ${field_id} on task ${task_id}.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error updating custom field: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
}
);
server.tool(
"updateMultipleCustomFields",
"Update multiple custom fields for a single task one by one (helper tool).",
{
task_id: z.string().min(6).max(9).describe("The 6-9 character task ID"),
updates: z.array(z.object({
field_id: z.string(),
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.record(z.any())])
})).describe("List of field updates to apply")
},
async ({ task_id, updates }) => {
const results: string[] = [];
for (const update of updates) {
try {
let finalValue = update.value;
if (typeof update.value === 'string' && !/^\d+$/.test(update.value) && Date.parse(update.value)) {
finalValue = new Date(update.value).getTime();
}
const response = await fetch(
`https://api.clickup.com/api/v2/task/${task_id}/field/${update.field_id}`,
{
method: 'POST',
headers: {
Authorization: CONFIG.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: finalValue })
}
);
if (response.ok) {
results.push(`Field ${update.field_id}: Success`);
} else {
results.push(`Field ${update.field_id}: Failed (${response.status})`);
}
} catch (e) {
results.push(`Field ${update.field_id}: Error (${e instanceof Error ? e.message : 'Unknown'})`);
}
}
return {
content: [{
type: "text",
text: `Batch Update Results:\n${results.join('\n')}`
}]
}
}
)
}