@hauptsache.net/clickup-mcp 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +62 -3
- package/dist/index.js +3 -1
- package/dist/shared/data-uri.d.ts +14 -0
- package/dist/shared/data-uri.d.ts.map +1 -0
- package/dist/shared/data-uri.js +34 -0
- package/dist/shared/image-processing.d.ts.map +1 -1
- package/dist/shared/image-processing.js +34 -7
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +199 -24
- package/package.json +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGpD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AA4BD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAAE,GAC3B,OAAO,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAkGrE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAClD,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CA0I5D"}
|
package/dist/clickup-text.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.processClickUpText = processClickUpText;
|
|
4
4
|
exports.processClickUpMarkdown = processClickUpMarkdown;
|
|
5
|
+
const data_uri_1 = require("./shared/data-uri");
|
|
5
6
|
/**
|
|
6
7
|
* Extract thumbnail URLs from data-attachment attribute JSON
|
|
7
8
|
* ClickUp API sometimes has broken thumbnail URLs, but data-attachment contains working ones
|
|
@@ -37,9 +38,38 @@ async function processClickUpText(textItems) {
|
|
|
37
38
|
const item = textItems[i];
|
|
38
39
|
// Handle image items
|
|
39
40
|
if (item.type === "image" && item.image && item.image.url) {
|
|
40
|
-
// Add image URL reference inline to current text block
|
|
41
41
|
const imageFileName = item.image.name || item.image.title || "image";
|
|
42
|
-
|
|
42
|
+
const imageUrl = item.image.url;
|
|
43
|
+
const altText = item.text || imageFileName;
|
|
44
|
+
if (imageUrl.startsWith("data:")) {
|
|
45
|
+
const parsedData = (0, data_uri_1.parseDataUri)(imageUrl);
|
|
46
|
+
currentTextBlock += `\nImage: ${imageFileName} - [inline image data]`;
|
|
47
|
+
if (currentTextBlock.trim()) {
|
|
48
|
+
contentBlocks.push({
|
|
49
|
+
type: "text",
|
|
50
|
+
text: currentTextBlock.trim(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
currentTextBlock = "";
|
|
54
|
+
if (parsedData) {
|
|
55
|
+
contentBlocks.push({
|
|
56
|
+
type: "image_metadata",
|
|
57
|
+
urls: [],
|
|
58
|
+
alt: altText,
|
|
59
|
+
inlineData: parsedData,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.error(`Unable to parse inline image data for ${imageFileName}`);
|
|
64
|
+
contentBlocks.push({
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `[Image "${altText}" omitted: unsupported inline data URI]`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Add image URL reference inline to current text block
|
|
72
|
+
currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
|
|
43
73
|
// Get working thumbnail URLs from data-attachment if available
|
|
44
74
|
const extractedThumbnails = extractThumbnailsFromDataAttachment(item.attributes);
|
|
45
75
|
// Determine best thumbnail URLs (prefer extracted over API thumbnails)
|
|
@@ -63,7 +93,7 @@ async function processClickUpText(textItems) {
|
|
|
63
93
|
contentBlocks.push({
|
|
64
94
|
type: "image_metadata",
|
|
65
95
|
urls: urls,
|
|
66
|
-
alt:
|
|
96
|
+
alt: altText,
|
|
67
97
|
});
|
|
68
98
|
}
|
|
69
99
|
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
|
|
@@ -110,6 +140,35 @@ function processClickUpMarkdown(markdownText, attachments) {
|
|
|
110
140
|
const [fullMatch, altText, imageUrl] = match;
|
|
111
141
|
// Add text before the image reference to the current text block
|
|
112
142
|
currentTextBlock += markdownText.substring(lastIndex, match.index);
|
|
143
|
+
if (imageUrl.startsWith("data:")) {
|
|
144
|
+
const imageFileName = altText || "image";
|
|
145
|
+
const parsedData = (0, data_uri_1.parseDataUri)(imageUrl);
|
|
146
|
+
currentTextBlock += `\nImage: ${imageFileName} - [inline image data]`;
|
|
147
|
+
if (currentTextBlock.trim()) {
|
|
148
|
+
contentBlocks.push({
|
|
149
|
+
type: "text",
|
|
150
|
+
text: currentTextBlock.trim(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
currentTextBlock = "";
|
|
154
|
+
if (parsedData) {
|
|
155
|
+
contentBlocks.push({
|
|
156
|
+
type: "image_metadata",
|
|
157
|
+
urls: [],
|
|
158
|
+
alt: altText || imageFileName,
|
|
159
|
+
inlineData: parsedData,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.error(`Unable to parse inline image data for ${imageFileName}`);
|
|
164
|
+
contentBlocks.push({
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `[Image "${altText || imageFileName}" omitted: unsupported inline data URI]`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
lastIndex = match.index + fullMatch.length;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
113
172
|
// Check if this image URL exists in our attachments
|
|
114
173
|
const attachment = attachmentMap.get(imageUrl);
|
|
115
174
|
if (attachment) {
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,9 @@ Use the ClickUp search tools to find tasks assigned to me, and get detailed info
|
|
|
123
123
|
const serverPromise = initializeServer();
|
|
124
124
|
exports.serverPromise = serverPromise;
|
|
125
125
|
// Only connect to the transport if this file is being run directly (not imported)
|
|
126
|
-
if (
|
|
126
|
+
// OR if not being imported by CLI (to support Claude Desktop's module loading)
|
|
127
|
+
const isCliMode = process.argv.some(arg => arg.includes('cli.ts') || arg.includes('cli.js'));
|
|
128
|
+
if (require.main === module || !isCliMode) {
|
|
127
129
|
// Start receiving messages on stdin and sending messages on stdout after initialization
|
|
128
130
|
serverPromise.then(() => {
|
|
129
131
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ParsedDataUri {
|
|
2
|
+
mimeType: string;
|
|
3
|
+
base64Data: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse data URI strings of the form data:mime/type;base64,....
|
|
7
|
+
* Returns null if the value is not a base64 data URI.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseDataUri(dataUri: string): ParsedDataUri | null;
|
|
10
|
+
/**
|
|
11
|
+
* Estimate the decoded byte-size of base64 data without allocating buffers
|
|
12
|
+
*/
|
|
13
|
+
export declare function estimateBase64Size(base64Data: string): number;
|
|
14
|
+
//# sourceMappingURL=data-uri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-uri.d.ts","sourceRoot":"","sources":["../../src/shared/data-uri.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseDataUri = parseDataUri;
|
|
4
|
+
exports.estimateBase64Size = estimateBase64Size;
|
|
5
|
+
/**
|
|
6
|
+
* Parse data URI strings of the form data:mime/type;base64,....
|
|
7
|
+
* Returns null if the value is not a base64 data URI.
|
|
8
|
+
*/
|
|
9
|
+
function parseDataUri(dataUri) {
|
|
10
|
+
if (!dataUri.startsWith("data:")) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const match = dataUri.match(/^data:([^;]+);base64,(.+)$/s);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const [, mimeType, base64Part] = match;
|
|
18
|
+
const sanitizedBase64 = base64Part.replace(/\s+/g, "");
|
|
19
|
+
if (!sanitizedBase64) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
mimeType,
|
|
24
|
+
base64Data: sanitizedBase64,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Estimate the decoded byte-size of base64 data without allocating buffers
|
|
29
|
+
*/
|
|
30
|
+
function estimateBase64Size(base64Data) {
|
|
31
|
+
const sanitized = base64Data.replace(/\s+/g, "");
|
|
32
|
+
const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
|
|
33
|
+
return Math.max(0, (sanitized.length * 3) / 4 - padding);
|
|
34
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-processing.d.ts","sourceRoot":"","sources":["../../src/shared/image-processing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"image-processing.d.ts","sourceRoot":"","sources":["../../src/shared/image-processing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAKzD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,EAAE,SAAS,GAAE,MAAyB,EAAE,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BhM"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.downloadImages = downloadImages;
|
|
4
4
|
const config_1 = require("./config");
|
|
5
|
+
const data_uri_1 = require("./data-uri");
|
|
5
6
|
const buffer_1 = require("buffer");
|
|
6
7
|
/**
|
|
7
8
|
* Downloads images from image_metadata blocks and applies smart size/count limiting
|
|
@@ -27,11 +28,12 @@ async function downloadImages(content, maxImages = config_1.CONFIG.maxImages, ma
|
|
|
27
28
|
// Download images in parallel and replace image_metadata blocks
|
|
28
29
|
const downloadPromises = countLimitedContent.map(async (block) => {
|
|
29
30
|
if (block.type === "image_metadata") {
|
|
31
|
+
if (block.inlineData) {
|
|
32
|
+
return convertInlineImage(block, perImageBudget);
|
|
33
|
+
}
|
|
30
34
|
return await downloadSingleImage(block, perImageBudget);
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
return block;
|
|
34
|
-
}
|
|
36
|
+
return block;
|
|
35
37
|
});
|
|
36
38
|
return Promise.all(downloadPromises);
|
|
37
39
|
}
|
|
@@ -68,10 +70,7 @@ function applyCountBasedLimitToImageMetadata(content, maxImages) {
|
|
|
68
70
|
* Download a single image from an image_metadata block, trying different sizes if needed
|
|
69
71
|
*/
|
|
70
72
|
async function downloadSingleImage(imageMetadata, perImageBudget) {
|
|
71
|
-
const fallbackText =
|
|
72
|
-
type: "text",
|
|
73
|
-
text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`,
|
|
74
|
-
};
|
|
73
|
+
const fallbackText = createImageFallback(imageMetadata);
|
|
75
74
|
// Try each URL in order (largest to smallest)
|
|
76
75
|
for (const url of imageMetadata.urls) {
|
|
77
76
|
const abortController = new AbortController();
|
|
@@ -124,3 +123,31 @@ async function downloadSingleImage(imageMetadata, perImageBudget) {
|
|
|
124
123
|
// If all URLs failed or were too large, return fallback text
|
|
125
124
|
return fallbackText;
|
|
126
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Create a fallback text block when an image cannot be included
|
|
128
|
+
*/
|
|
129
|
+
function createImageFallback(imageMetadata) {
|
|
130
|
+
return {
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convert inline image data URIs into image blocks while respecting size budgets
|
|
137
|
+
*/
|
|
138
|
+
function convertInlineImage(imageMetadata, perImageBudget) {
|
|
139
|
+
const inlineData = imageMetadata.inlineData;
|
|
140
|
+
if (!inlineData) {
|
|
141
|
+
return createImageFallback(imageMetadata);
|
|
142
|
+
}
|
|
143
|
+
const estimatedSize = (0, data_uri_1.estimateBase64Size)(inlineData.base64Data);
|
|
144
|
+
if (perImageBudget <= 0 || estimatedSize > perImageBudget) {
|
|
145
|
+
console.error(`Inline image for "${imageMetadata.alt}" is ${estimatedSize} bytes, exceeds budget of ${perImageBudget} bytes`);
|
|
146
|
+
return createImageFallback(imageMetadata);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
type: "image",
|
|
150
|
+
mimeType: inlineData.mimeType || "image/png",
|
|
151
|
+
data: inlineData.base64Data,
|
|
152
|
+
};
|
|
153
|
+
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
|
+
import { ParsedDataUri } from "./data-uri";
|
|
2
3
|
export type ContentBlock = CallToolResult['content'][number];
|
|
3
4
|
export interface ImageMetadataBlock {
|
|
4
5
|
type: "image_metadata";
|
|
5
6
|
urls: string[];
|
|
6
7
|
alt: string;
|
|
8
|
+
inlineData?: ParsedDataUri;
|
|
7
9
|
}
|
|
8
10
|
export interface DatedContentEvent {
|
|
9
11
|
date: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAGD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,CAAC;CACtD;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAapE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAapE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QA2YtE"}
|
|
@@ -81,7 +81,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
81
81
|
});
|
|
82
82
|
server.tool("updateTask", (() => {
|
|
83
83
|
const descriptionBase = [
|
|
84
|
-
"Updates various aspects of an existing task.",
|
|
84
|
+
"Updates various aspects of an existing task including dependencies and relationships.",
|
|
85
85
|
"ALWAYS include the task URL (https://app.clickup.com/t/TASK_ID) when updating or referencing tasks.",
|
|
86
86
|
"Use getListInfo first to see valid status options.",
|
|
87
87
|
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
@@ -106,10 +106,13 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
106
106
|
time_estimate: taskTimeEstimateSchema,
|
|
107
107
|
tags: taskTagsSchema.describe("Optional array of tag names (will replace existing tags)"),
|
|
108
108
|
parent_task_id: zod_1.z.string().optional().describe("Optional parent task ID to change parent/child relationships"),
|
|
109
|
-
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
|
|
109
|
+
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData)),
|
|
110
|
+
waiting_on: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs that this task should wait on (will replace existing waiting_on relationships)"),
|
|
111
|
+
blocking: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs that this task should block. Note: This creates dependencies FROM those tasks TO this task (those tasks will wait on this one)"),
|
|
112
|
+
linked_tasks: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs to link as related tasks without blocking (will replace existing linked tasks)")
|
|
110
113
|
}, {
|
|
111
114
|
readOnlyHint: false
|
|
112
|
-
}, async ({ task_id, name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees }) => {
|
|
115
|
+
}, async ({ task_id, name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees, blocking, waiting_on, linked_tasks }) => {
|
|
113
116
|
try {
|
|
114
117
|
const userData = await (0, utils_1.getCurrentUser)();
|
|
115
118
|
// Get task details including current markdown description
|
|
@@ -120,6 +123,54 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
120
123
|
throw new Error(`Error fetching task: ${taskResponse.status} ${taskResponse.statusText}`);
|
|
121
124
|
}
|
|
122
125
|
const taskData = await taskResponse.json();
|
|
126
|
+
// Handle dependencies separately since they need individual API calls
|
|
127
|
+
let dependencyUpdateResults = [];
|
|
128
|
+
if (blocking !== undefined || waiting_on !== undefined || linked_tasks !== undefined) {
|
|
129
|
+
const dependencyResults = await updateTaskDependencies(task_id, taskData, { blocking, waiting_on, linked_tasks });
|
|
130
|
+
dependencyUpdateResults = dependencyResults;
|
|
131
|
+
}
|
|
132
|
+
// Handle tags separately since they need individual API calls
|
|
133
|
+
let tagUpdateResults = [];
|
|
134
|
+
if (tags !== undefined) {
|
|
135
|
+
// Get current tags
|
|
136
|
+
const currentTags = taskData.tags?.map((t) => t.name) || [];
|
|
137
|
+
const tagsToAdd = tags.filter(tag => !currentTags.includes(tag));
|
|
138
|
+
const tagsToRemove = currentTags.filter((tag) => !tags.includes(tag));
|
|
139
|
+
// Add new tags
|
|
140
|
+
for (const tagName of tagsToAdd) {
|
|
141
|
+
try {
|
|
142
|
+
const addTagResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { Authorization: config_1.CONFIG.apiKey }
|
|
145
|
+
});
|
|
146
|
+
if (!addTagResponse.ok) {
|
|
147
|
+
console.error(`Failed to add tag "${tagName}": ${addTagResponse.status}`);
|
|
148
|
+
tagUpdateResults.push(`Failed to add tag: ${tagName}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error(`Error adding tag "${tagName}":`, error);
|
|
153
|
+
tagUpdateResults.push(`Error adding tag: ${tagName}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Remove old tags
|
|
157
|
+
for (const tagName of tagsToRemove) {
|
|
158
|
+
try {
|
|
159
|
+
const removeTagResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/tag/${encodeURIComponent(tagName)}`, {
|
|
160
|
+
method: 'DELETE',
|
|
161
|
+
headers: { Authorization: config_1.CONFIG.apiKey }
|
|
162
|
+
});
|
|
163
|
+
if (!removeTagResponse.ok) {
|
|
164
|
+
console.error(`Failed to remove tag "${tagName}": ${removeTagResponse.status}`);
|
|
165
|
+
tagUpdateResults.push(`Failed to remove tag: ${tagName}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error(`Error removing tag "${tagName}":`, error);
|
|
170
|
+
tagUpdateResults.push(`Error removing tag: ${tagName}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
123
174
|
// Handle append-only description update with markdown support
|
|
124
175
|
let finalDescription;
|
|
125
176
|
if (append_description) {
|
|
@@ -128,9 +179,9 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
128
179
|
const separator = currentDescription.trim() ? "\n\n---\n" : "";
|
|
129
180
|
finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${append_description}`;
|
|
130
181
|
}
|
|
131
|
-
// Build update body
|
|
182
|
+
// Build update body without tags (they're handled separately)
|
|
132
183
|
const updateBody = buildTaskRequestBody({
|
|
133
|
-
name, status, priority, due_date, start_date, time_estimate,
|
|
184
|
+
name, status, priority, due_date, start_date, time_estimate, parent_task_id, assignees
|
|
134
185
|
});
|
|
135
186
|
// Add markdown description if we have content to append
|
|
136
187
|
if (finalDescription !== undefined) {
|
|
@@ -140,8 +191,8 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
140
191
|
if (assignees !== undefined) {
|
|
141
192
|
updateBody.assignees = { add: assignees, rem: [] }; // Add new assignees, remove none
|
|
142
193
|
}
|
|
143
|
-
// Check if there's anything to update
|
|
144
|
-
if (Object.keys(updateBody).length === 0) {
|
|
194
|
+
// Check if there's anything to update (including tags and dependencies which were handled separately)
|
|
195
|
+
if (Object.keys(updateBody).length === 0 && tags === undefined && blocking === undefined && waiting_on === undefined && linked_tasks === undefined) {
|
|
145
196
|
return {
|
|
146
197
|
content: [
|
|
147
198
|
{
|
|
@@ -151,23 +202,43 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
151
202
|
],
|
|
152
203
|
};
|
|
153
204
|
}
|
|
154
|
-
// Update the task
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
205
|
+
// Update the task (if there are non-tag updates)
|
|
206
|
+
let updatedTask = taskData;
|
|
207
|
+
if (Object.keys(updateBody).length > 0) {
|
|
208
|
+
const updateResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
|
|
209
|
+
method: 'PUT',
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
212
|
+
'Content-Type': 'application/json'
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify(updateBody)
|
|
215
|
+
});
|
|
216
|
+
if (!updateResponse.ok) {
|
|
217
|
+
const errorData = await updateResponse.json().catch(() => ({}));
|
|
218
|
+
throw new Error(`Error updating task: ${updateResponse.status} ${updateResponse.statusText} - ${JSON.stringify(errorData)}`);
|
|
219
|
+
}
|
|
220
|
+
updatedTask = await updateResponse.json();
|
|
221
|
+
}
|
|
222
|
+
// If only tags or dependencies were updated, fetch the task again to get the updated state
|
|
223
|
+
if ((tags !== undefined || blocking !== undefined || waiting_on !== undefined || linked_tasks !== undefined) && Object.keys(updateBody).length === 0) {
|
|
224
|
+
const refreshResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
|
|
225
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
226
|
+
});
|
|
227
|
+
if (refreshResponse.ok) {
|
|
228
|
+
updatedTask = await refreshResponse.json();
|
|
229
|
+
}
|
|
166
230
|
}
|
|
167
|
-
const updatedTask = await updateResponse.json();
|
|
168
231
|
const responseLines = formatTaskResponse(updatedTask, 'updated', {
|
|
169
|
-
name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees
|
|
232
|
+
name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees, blocking, waiting_on, linked_tasks
|
|
170
233
|
}, userData);
|
|
234
|
+
// Add dependency update results if any
|
|
235
|
+
if (dependencyUpdateResults.length > 0) {
|
|
236
|
+
responseLines.push('dependency_warnings: ' + dependencyUpdateResults.join('; '));
|
|
237
|
+
}
|
|
238
|
+
// Add tag update results if any
|
|
239
|
+
if (tagUpdateResults.length > 0) {
|
|
240
|
+
responseLines.push('tag_warnings: ' + tagUpdateResults.join('; '));
|
|
241
|
+
}
|
|
171
242
|
return {
|
|
172
243
|
content: [
|
|
173
244
|
{
|
|
@@ -329,9 +400,8 @@ function buildTaskRequestBody(params, currentUserId) {
|
|
|
329
400
|
if (params.time_estimate !== undefined) {
|
|
330
401
|
requestBody.time_estimate = Math.round(params.time_estimate * 60 * 60 * 1000);
|
|
331
402
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
403
|
+
// Tags are handled separately via dedicated API endpoints
|
|
404
|
+
// Do not include in the update request body
|
|
335
405
|
if (params.assignees !== undefined) {
|
|
336
406
|
requestBody.assignees = params.assignees;
|
|
337
407
|
}
|
|
@@ -343,6 +413,102 @@ function buildTaskRequestBody(params, currentUserId) {
|
|
|
343
413
|
}
|
|
344
414
|
return requestBody;
|
|
345
415
|
}
|
|
416
|
+
// Helper function to manage task dependencies
|
|
417
|
+
async function updateTaskDependencies(taskId, taskData, dependencies) {
|
|
418
|
+
const errors = [];
|
|
419
|
+
// Get current dependencies
|
|
420
|
+
const currentBlocking = taskData.blocking?.map((dep) => dep.id) || [];
|
|
421
|
+
const currentWaitingOn = taskData.waiting_on?.map((dep) => dep.id) || [];
|
|
422
|
+
const currentLinked = taskData.linked_tasks?.map((task) => task.id) || [];
|
|
423
|
+
// Helper function to make dependency API calls
|
|
424
|
+
async function modifyDependency(operation, type, fromTaskId, toTaskId, dependsOn) {
|
|
425
|
+
try {
|
|
426
|
+
let url;
|
|
427
|
+
let options;
|
|
428
|
+
if (type === 'linked') {
|
|
429
|
+
// Linked tasks use a different endpoint
|
|
430
|
+
url = `https://api.clickup.com/api/v2/task/${fromTaskId}/link/${toTaskId}`;
|
|
431
|
+
options = {
|
|
432
|
+
method: operation === 'add' ? 'POST' : 'DELETE',
|
|
433
|
+
headers: { Authorization: config_1.CONFIG.apiKey }
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Dependencies (blocking/waiting_on)
|
|
438
|
+
if (operation === 'add') {
|
|
439
|
+
url = `https://api.clickup.com/api/v2/task/${fromTaskId}/dependency`;
|
|
440
|
+
options = {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: {
|
|
443
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
444
|
+
'Content-Type': 'application/json'
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify({
|
|
447
|
+
depends_on: dependsOn,
|
|
448
|
+
dependency_type: 1 // Always use 1 for waiting_on type
|
|
449
|
+
})
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Remove dependency
|
|
454
|
+
url = `https://api.clickup.com/api/v2/task/${fromTaskId}/dependency?depends_on=${dependsOn}`;
|
|
455
|
+
options = {
|
|
456
|
+
method: 'DELETE',
|
|
457
|
+
headers: { Authorization: config_1.CONFIG.apiKey }
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const response = await fetch(url, options);
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
const action = operation === 'add' ? 'add' : 'remove';
|
|
464
|
+
const typeLabel = type === 'linked' ? 'link' : type.replace('_', ' ');
|
|
465
|
+
console.error(`Failed to ${action} ${typeLabel} "${toTaskId}": ${response.status}`);
|
|
466
|
+
errors.push(`Failed to ${action} ${typeLabel}: ${toTaskId}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
const action = operation === 'add' ? 'adding' : 'removing';
|
|
471
|
+
const typeLabel = type === 'linked' ? 'link' : type.replace('_', ' ');
|
|
472
|
+
console.error(`Error ${action} ${typeLabel} "${toTaskId}":`, error);
|
|
473
|
+
errors.push(`Error ${action} ${typeLabel}: ${toTaskId}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Process blocking dependencies (tasks that should depend on this task)
|
|
477
|
+
if (dependencies.blocking !== undefined) {
|
|
478
|
+
const toAdd = dependencies.blocking.filter(id => !currentBlocking.includes(id));
|
|
479
|
+
const toRemove = currentBlocking.filter((id) => !dependencies.blocking.includes(id));
|
|
480
|
+
// To make another task depend on this one, add dependency FROM that task TO this task
|
|
481
|
+
for (const depTaskId of toAdd) {
|
|
482
|
+
await modifyDependency('add', 'blocking', depTaskId, depTaskId, taskId);
|
|
483
|
+
}
|
|
484
|
+
for (const depTaskId of toRemove) {
|
|
485
|
+
await modifyDependency('remove', 'blocking', depTaskId, depTaskId, taskId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Process waiting_on dependencies
|
|
489
|
+
if (dependencies.waiting_on !== undefined) {
|
|
490
|
+
const toAdd = dependencies.waiting_on.filter(id => !currentWaitingOn.includes(id));
|
|
491
|
+
const toRemove = currentWaitingOn.filter((id) => !dependencies.waiting_on.includes(id));
|
|
492
|
+
for (const depTaskId of toAdd) {
|
|
493
|
+
await modifyDependency('add', 'waiting_on', taskId, depTaskId, depTaskId);
|
|
494
|
+
}
|
|
495
|
+
for (const depTaskId of toRemove) {
|
|
496
|
+
await modifyDependency('remove', 'waiting_on', taskId, depTaskId, depTaskId);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Process linked tasks
|
|
500
|
+
if (dependencies.linked_tasks !== undefined) {
|
|
501
|
+
const toAdd = dependencies.linked_tasks.filter(id => !currentLinked.includes(id));
|
|
502
|
+
const toRemove = currentLinked.filter((id) => !dependencies.linked_tasks.includes(id));
|
|
503
|
+
for (const linkedTaskId of toAdd) {
|
|
504
|
+
await modifyDependency('add', 'linked', taskId, linkedTaskId, linkedTaskId);
|
|
505
|
+
}
|
|
506
|
+
for (const linkedTaskId of toRemove) {
|
|
507
|
+
await modifyDependency('remove', 'linked', taskId, linkedTaskId, linkedTaskId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return errors;
|
|
511
|
+
}
|
|
346
512
|
function formatTaskResponse(task, operation, params, userData) {
|
|
347
513
|
const responseLines = [
|
|
348
514
|
`Task ${operation} successfully!`,
|
|
@@ -377,5 +543,14 @@ function formatTaskResponse(task, operation, params, userData) {
|
|
|
377
543
|
if (params.parent_task_id !== undefined) {
|
|
378
544
|
responseLines.push(`parent_task_id: ${params.parent_task_id}`);
|
|
379
545
|
}
|
|
546
|
+
if (params.blocking !== undefined && params.blocking.length > 0) {
|
|
547
|
+
responseLines.push(`blocking: ${params.blocking.join(', ')}`);
|
|
548
|
+
}
|
|
549
|
+
if (params.waiting_on !== undefined && params.waiting_on.length > 0) {
|
|
550
|
+
responseLines.push(`waiting_on: ${params.waiting_on.join(', ')}`);
|
|
551
|
+
}
|
|
552
|
+
if (params.linked_tasks !== undefined && params.linked_tasks.length > 0) {
|
|
553
|
+
responseLines.push(`linked_tasks: ${params.linked_tasks.join(', ')}`);
|
|
554
|
+
}
|
|
380
555
|
return responseLines;
|
|
381
556
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hauptsache.net/clickup-mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Search, create, and retrieve tasks, add comments, and track time through natural language commands.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"cli": "npx ts-node src/cli.ts",
|
|
17
17
|
"prettier": "prettier --write src/**/*.ts",
|
|
18
18
|
"prepublishOnly": "rm -r dist && npm run build",
|
|
19
|
-
"release": "npm run build && npm publish --access public && git add . && git commit -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git tag -a v$(node -p 'require(\"./package.json\").version') -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git push && git push --tags"
|
|
19
|
+
"release": "npm run build && npm publish --access public && git add . && git commit -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git tag -a v$(node -p 'require(\"./package.json\").version') -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git push && git push --tags",
|
|
20
|
+
"dxt": "npm run build && npx dxt pack",
|
|
21
|
+
"test": "node --test -r ts-node/register src/**/*.test.ts"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
22
24
|
"clickup",
|
|
@@ -47,7 +49,9 @@
|
|
|
47
49
|
"dotenv": "^16.5.0",
|
|
48
50
|
"nodemon": "^3.1.9",
|
|
49
51
|
"prettier": "^3.5.3",
|
|
50
|
-
"
|
|
52
|
+
"ts-node": "^10.9.2",
|
|
53
|
+
"typescript": "^5.8.3",
|
|
54
|
+
"undici": "^7.16.0"
|
|
51
55
|
},
|
|
52
56
|
"engines": {
|
|
53
57
|
"node": ">=16.0.0"
|