Skip to content

Commit 082362a

Browse files
feat: add support for custom task IDs (e.g., CHIEF-5804)
All task-related tools now accept custom task IDs in addition to internal IDs. When a custom task ID is detected (format: PREFIX-NUMBER), the API calls automatically include custom_task_ids=true and team_id parameters as required by the ClickUp API. Changes: - Add isCustomTaskId() helper function to detect custom ID format - Update isTaskId() to validate both internal and custom ID formats - Relax task_id validation across all tools (getTaskById, updateTask, addComment, searchTasks, getTimeEntries, createTimeEntry) - Inject custom_task_ids=true and team_id params for API calls with custom IDs - Update tool descriptions to document custom ID support Co-Authored-By: Claude <[email protected]>
1 parent ed278f9 commit 082362a

File tree

6 files changed

+70
-25
lines changed

6 files changed

+70
-25
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **Custom task ID support** - All task-related tools now accept custom task IDs (e.g., "CHIEF-5804") in addition to internal IDs (e.g., "86b852ppx")
12+
- New `isCustomTaskId()` helper function to detect custom task ID format
13+
- Automatic `custom_task_ids=true` and `team_id` parameter injection for API calls when custom IDs are used
14+
15+
### Changed
16+
- Relaxed task ID validation across all tools (`getTaskById`, `updateTask`, `addComment`, `searchTasks`, `getTimeEntries`, `createTimeEntry`) to accept both internal and custom ID formats
17+
- Updated tool descriptions to document custom ID support
18+
819
## [1.6.0] - 2025-11-25
920

1021
### Added

src/shared/utils.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,28 @@ const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit ti
55

66
/**
77
* Checks if a string looks like a valid ClickUp task ID
8-
* Valid task IDs are 6-9 characters long and contain only alphanumeric characters
8+
* Valid task IDs are either:
9+
* - Internal IDs: 6-9 alphanumeric characters (e.g., "86b852ppx")
10+
* - Custom IDs: PREFIX-NUMBER format (e.g., "CHIEF-5804")
911
*/
1012
export function isTaskId(str: string): boolean {
11-
// Task IDs are 6-9 characters long and contain only alphanumeric characters
12-
return /^[a-z0-9]{6,9}$/i.test(str);
13+
// Internal task IDs: 6-9 alphanumeric characters
14+
if (/^[a-z0-9]{6,9}$/i.test(str)) {
15+
return true;
16+
}
17+
// Custom task IDs: PREFIX-NUMBER format (e.g., "CHIEF-5804", "PROJ-123")
18+
if (/^[A-Z]+-\d+$/i.test(str)) {
19+
return true;
20+
}
21+
return false;
22+
}
23+
24+
/**
25+
* Checks if a task ID is a custom task ID (vs internal ID)
26+
* Custom task IDs require special API parameters
27+
*/
28+
export function isCustomTaskId(str: string): boolean {
29+
return /^[A-Z]+-\d+$/i.test(str);
1330
}
1431

1532
// Cache for current user info to avoid repeated API calls and race conditions

src/tools/search-tools.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
22
import {z} from "zod";
33
import {CONFIG} from "../shared/config";
4-
import {isTaskId, getTaskSearchIndex, performMultiTermSearch} from "../shared/utils";
4+
import {isTaskId, isCustomTaskId, getTaskSearchIndex, performMultiTermSearch} from "../shared/utils";
55
import {generateTaskMetadata} from "./task-tools";
66

77
const MAX_SEARCH_RESULTS = 50;
@@ -131,8 +131,9 @@ export function registerSearchTools(server: McpServer, userData: any) {
131131
console.error(`Attempting direct fetch for task IDs: ${taskIdsToFetchDirectly.join(', ')}`);
132132
const directFetchPromises = taskIdsToFetchDirectly.map(async (id) => {
133133
try {
134+
const customIdParams = isCustomTaskId(id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
134135
const response = await fetch(
135-
`https://api.clickup.com/api/v2/task/${id}`,
136+
`https://api.clickup.com/api/v2/task/${id}${customIdParams}`,
136137
{headers: {Authorization: CONFIG.apiKey}}
137138
);
138139
if (response.ok) {

src/tools/task-tools.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from "zod";
33
import { convertMarkdownToToolCallResult, convertClickUpTextItemsToToolCallResult } from "../clickup-text";
44
import { ContentBlock, DatedContentEvent, ImageMetadataBlock } from "../shared/types";
55
import { CONFIG } from "../shared/config";
6-
import { isTaskId, getSpaceDetails, getAllTeamMembers } from "../shared/utils";
6+
import { isTaskId, isCustomTaskId, getSpaceDetails, getAllTeamMembers } from "../shared/utils";
77
import { downloadImages } from "../shared/image-processing";
88

99
// Read-specific utility functions
@@ -19,13 +19,12 @@ export function registerTaskToolsRead(server: McpServer, userData: any) {
1919
{
2020
id: z
2121
.string()
22-
.min(6)
23-
.max(9)
22+
.min(1)
2423
.refine(val => isTaskId(val), {
25-
message: "Task ID must be 6-9 alphanumeric characters only"
24+
message: "Task ID must be either 6-9 alphanumeric characters or a custom ID like 'CHIEF-5804'"
2625
})
2726
.describe(
28-
`The 6-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`
27+
`The task ID - either internal (6-9 chars like "86b852ppx") or custom (like "CHIEF-5804"). Do not include prefixes like "#", "CU-" or full URLs.`
2928
),
3029
},
3130
{
@@ -104,8 +103,9 @@ async function fetchTaskTimeEntries(taskId: string): Promise<any[]> {
104103
}
105104

106105
async function loadTaskContent(taskId: string): Promise<(ContentBlock | ImageMetadataBlock)[]> {
106+
const customIdParams = isCustomTaskId(taskId) ? `&custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
107107
const response = await fetch(
108-
`https://api.clickup.com/api/v2/task/${taskId}?include_markdown_description=true&include_subtasks=true`,
108+
`https://api.clickup.com/api/v2/task/${taskId}?include_markdown_description=true&include_subtasks=true${customIdParams}`,
109109
{ headers: { Authorization: CONFIG.apiKey } }
110110
);
111111
const task = await response.json();
@@ -127,8 +127,9 @@ async function loadTaskContent(taskId: string): Promise<(ContentBlock | ImageMet
127127
}
128128

129129
async function loadTaskComments(id: string): Promise<DatedContentEvent[]> {
130+
const customIdParams = isCustomTaskId(id) ? `&custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
130131
const response = await fetch(
131-
`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0`, // Ensure all comments are fetched
132+
`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0${customIdParams}`, // Ensure all comments are fetched
132133
{ headers: { Authorization: CONFIG.apiKey } }
133134
);
134135
if (!response.ok) {
@@ -159,7 +160,8 @@ async function loadTaskComments(id: string): Promise<DatedContentEvent[]> {
159160
}
160161

161162
async function loadTimeInStatusHistory(taskId: string): Promise<DatedContentEvent[]> {
162-
const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status`;
163+
const customIdParams = isCustomTaskId(taskId) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
164+
const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status${customIdParams}`;
163165
try {
164166
const response = await fetch(url, { headers: { Authorization: CONFIG.apiKey } });
165167
if (!response.ok) {

src/tools/task-write-tools.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { z } from "zod";
33
import { CONFIG } from "../shared/config";
4-
import { getCurrentUser } from "../shared/utils";
4+
import { getCurrentUser, isTaskId, isCustomTaskId } from "../shared/utils";
55
import { convertMarkdownToClickUpBlocks } from "../clickup-text";
66

77
// Shared schemas for task parameters
@@ -35,7 +35,9 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
3535
return descriptionBase.join("\n");
3636
})(),
3737
{
38-
task_id: z.string().min(6).max(9).describe("The 6-9 character task ID to comment on"),
38+
task_id: z.string().min(1).refine(val => isTaskId(val), {
39+
message: "Task ID must be either 6-9 alphanumeric characters or a custom ID like 'CHIEF-5804'"
40+
}).describe("The task ID to comment on (internal like '86b852ppx' or custom like 'CHIEF-5804')"),
3941
comment: z.string().min(1).describe("The comment text to add to the task"),
4042
},
4143
{
@@ -53,7 +55,8 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
5355
notify_all: true
5456
};
5557

56-
const response = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/comment`, {
58+
const customIdParams = isCustomTaskId(task_id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
59+
const response = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/comment${customIdParams}`, {
5760
method: 'POST',
5861
headers: {
5962
Authorization: CONFIG.apiKey,
@@ -122,7 +125,9 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
122125
return descriptionBase.join("\n");
123126
})(),
124127
{
125-
task_id: z.string().min(6).max(9).describe("The 6-9 character task ID to update"),
128+
task_id: z.string().min(1).refine(val => isTaskId(val), {
129+
message: "Task ID must be either 6-9 alphanumeric characters or a custom ID like 'CHIEF-5804'"
130+
}).describe("The task ID to update (internal like '86b852ppx' or custom like 'CHIEF-5804')"),
126131
name: taskNameSchema.optional(),
127132
append_description: z.string().optional().describe("Optional markdown content to APPEND to existing task description (preserves existing content for safety)"),
128133
status: z.string().optional().describe("Optional new status name - use getListInfo to see valid options"),
@@ -148,7 +153,8 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
148153
const userData = await getCurrentUser();
149154

150155
// Get task details including current markdown description
151-
const taskResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}?include_markdown_description=true`, {
156+
const customIdParams = isCustomTaskId(task_id) ? `&custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
157+
const taskResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}?include_markdown_description=true${customIdParams}`, {
152158
headers: { Authorization: CONFIG.apiKey },
153159
});
154160

@@ -180,8 +186,9 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
180186
// Add new tags
181187
for (const tagName of tagsToAdd) {
182188
try {
189+
const tagCustomIdParams = isCustomTaskId(task_id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
183190
const addTagResponse = await fetch(
184-
`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}`,
191+
`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}${tagCustomIdParams}`,
185192
{
186193
method: 'POST',
187194
headers: { Authorization: CONFIG.apiKey }
@@ -200,8 +207,9 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
200207
// Remove old tags
201208
for (const tagName of tagsToRemove) {
202209
try {
210+
const tagCustomIdParams = isCustomTaskId(task_id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
203211
const removeTagResponse = await fetch(
204-
`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}`,
212+
`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}${tagCustomIdParams}`,
205213
{
206214
method: 'DELETE',
207215
headers: { Authorization: CONFIG.apiKey }
@@ -257,7 +265,8 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
257265
// Update the task (if there are non-tag updates)
258266
let updatedTask = taskData;
259267
if (Object.keys(updateBody).length > 0) {
260-
const updateResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
268+
const updateCustomIdParams = isCustomTaskId(task_id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
269+
const updateResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}${updateCustomIdParams}`, {
261270
method: 'PUT',
262271
headers: {
263272
Authorization: CONFIG.apiKey,
@@ -276,7 +285,8 @@ export function registerTaskToolsWrite(server: McpServer, userData: any) {
276285

277286
// If only tags or dependencies were updated, fetch the task again to get the updated state
278287
if ((tags !== undefined || blocking !== undefined || waiting_on !== undefined || linked_tasks !== undefined) && Object.keys(updateBody).length === 0) {
279-
const refreshResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
288+
const refreshCustomIdParams = isCustomTaskId(task_id) ? `?custom_task_ids=true&team_id=${CONFIG.teamId}` : '';
289+
const refreshResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}${refreshCustomIdParams}`, {
280290
headers: { Authorization: CONFIG.apiKey },
281291
});
282292
if (refreshResponse.ok) {

src/tools/time-tools.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { z } from "zod";
33
import { CONFIG } from "../shared/config";
4-
import { getAllTeamMembers } from "../shared/utils";
4+
import { getAllTeamMembers, isTaskId } from "../shared/utils";
55

66
/**
77
* Converts ISO date string to Unix timestamp in milliseconds
@@ -60,7 +60,9 @@ export function registerTimeToolsRead(server: McpServer) {
6060
"getTimeEntries",
6161
"Gets time entries for a specific task or all user's time entries. Returns last 30 days by default if no dates specified.",
6262
{
63-
task_id: z.string().min(6).max(9).optional().describe("Optional 6-9 character task ID to filter entries. If not provided, returns all user's time entries."),
63+
task_id: z.string().min(1).refine(val => isTaskId(val), {
64+
message: "Task ID must be either 6-9 alphanumeric characters or a custom ID like 'CHIEF-5804'"
65+
}).optional().describe("Optional task ID to filter entries (internal like '86b852ppx' or custom like 'CHIEF-5804'). If not provided, returns all user's time entries."),
6466
start_date: z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00'). Defaults to 30 days ago."),
6567
end_date: z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date."),
6668
list_id: z.string().optional().describe("Optional single list ID to filter time entries by a specific list"),
@@ -329,7 +331,9 @@ export function registerTimeToolsWrite(server: McpServer) {
329331
"Suggest moving the task to an active status like 'in progress' first."
330332
].join("\n"),
331333
{
332-
task_id: z.string().min(6).max(9).describe("The 6-9 character task ID to book time against"),
334+
task_id: z.string().min(1).refine(val => isTaskId(val), {
335+
message: "Task ID must be either 6-9 alphanumeric characters or a custom ID like 'CHIEF-5804'"
336+
}).describe("The task ID to book time against (internal like '86b852ppx' or custom like 'CHIEF-5804')"),
333337
hours: z.number().min(0.01).max(24).describe("Hours to book (decimal format, e.g., 0.25 = 15min, 1.5 = 1h 30min)"),
334338
description: z.string().optional().describe("Optional description for the time entry"),
335339
start_time: z.string().optional().describe("Optional start time as ISO date string (e.g., '2024-10-06T09:00:00+02:00', defaults to current time)")

0 commit comments

Comments
 (0)