@hauptsache.net/clickup-mcp 1.4.1 → 1.4.3

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/README.md CHANGED
@@ -74,38 +74,81 @@ Turn natural language into powerful ClickUp actions:
74
74
  - **Append-Only Descriptions**: Description fields are never overwritten - new content is safely appended with timestamps
75
75
  - **Normal Field Updates**: Status, priority, assignees, tags, and dates can be updated normally (easily revertible through ClickUp's history)
76
76
 
77
- ## Setup for Claude Desktop, Windsurf, or Cursor
78
-
79
- 1. **Prerequisites**:
80
- - Your `CLICKUP_API_KEY` (Profile Icon > Settings > Apps > API Token ~ usually starts with pk_)
81
- - and your `CLICKUP_TEAM_ID` (The ~7 digit number in the url when you are in the settings)
82
-
83
- 2. **Configuration**:
84
- Add the following to your MCP configuration file:
85
-
86
- ```json
87
- {
88
- "mcpServers": {
89
- "clickup": {
90
- "command": "npx",
91
- "args": [
92
- "@hauptsache.net/clickup-mcp@latest"
93
- ],
94
- "env": {
95
- "CLICKUP_API_KEY": "your_api_key",
96
- "CLICKUP_TEAM_ID": "your_team_id"
97
- }
98
- }
99
- }
100
- }
101
- ```
102
-
103
- Replace `your_api_key` and `your_team_id` with your actual ClickUp credentials.
104
-
105
- 3. **Connect Your AI Assistant**:
106
- - **Claude Desktop**: Add this configuration in Settings > Developer > Edit Config
107
- - **Windsurf**: Add to your MCP configuration file
108
- - **Cursor**: Configure through the MCP settings panel
77
+ ## Installation
78
+
79
+ ### Prerequisites
80
+
81
+ For all installation methods, you'll need:
82
+ - Your `CLICKUP_API_KEY` (Profile Icon > Settings > Apps > API Token ~ usually starts with pk_)
83
+ - Your `CLICKUP_TEAM_ID` (The 7–10 digit number in the URL when you are in the settings)
84
+
85
+ ### Option 1: MCPB Bundle (Recommended for Claude Desktop)
86
+
87
+ Download the pre-built bundle from our [releases page](https://github.com/hauptsacheNet/clickup-mcp/releases). This method requires no Node.js installation.
88
+
89
+ You'll get a configuration screen where you are prompted to enter your API key and team ID.
90
+
91
+ ### Option 2: NPX Installation
92
+
93
+ This method automatically updates to the latest version and is preferred for users who want the newest features.
94
+
95
+ **For Claude Desktop, Windsurf, Cursor and others:**
96
+
97
+ Add the following to your MCP configuration file:
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "clickup": {
103
+ "command": "npx",
104
+ "args": [
105
+ "@hauptsache.net/clickup-mcp@latest"
106
+ ],
107
+ "env": {
108
+ "CLICKUP_API_KEY": "your_api_key",
109
+ "CLICKUP_TEAM_ID": "your_team_id"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ Replace `your_api_key` and `your_team_id` with your actual ClickUp credentials.
117
+
118
+ **Where to add this configuration:**
119
+ - **Claude Desktop**: Settings > Developer > Edit Config
120
+ - **Windsurf**: Add to your MCP configuration file
121
+ - **Cursor**: Configure through the MCP settings panel
122
+
123
+ ### Option 3: Coding Tools Integration
124
+
125
+ **Claude Code (CLI):**
126
+ ```bash
127
+ claude mcp add clickup \
128
+ --env CLICKUP_API_KEY=YOUR_KEY \
129
+ --env CLICKUP_TEAM_ID=YOUR_ID \
130
+ --env CLICKUP_MCP_MODE=read-minimal \
131
+ --env MAX_IMAGES=16 \
132
+ --env MAX_RESPONSE_SIZE_MB=4 \
133
+ -- npx -y @hauptsache.net/clickup-mcp
134
+ ```
135
+
136
+ > Claude Code can handle a lot of images, thus the recommended increased limits.
137
+
138
+ > Note the `CLICKUP_MCP_MODE=read-minimal`. This is my usage recommendation, but feel free to use one of the other modes.
139
+
140
+ **OpenAI Codex:**
141
+ Add these lines to your `~/.codex/config.toml` file:
142
+ ```toml
143
+ [mcp_servers.clickup]
144
+ command = "npx"
145
+ args = ["-y", "@hauptsache.net/clickup-mcp@latest"]
146
+ env = { "CLICKUP_API_KEY" = "YOUR_KEY", "CLICKUP_TEAM_ID" = "YOUR_ID", "CLICKUP_MCP_MODE" = "read-minimal" }
147
+ ```
148
+
149
+ > Codex seems to not be able to handle images from MCP's. See [this issue](https://github.com/openai/codex/issues/3741) for more details.
150
+
151
+ > Note the `CLICKUP_MCP_MODE=read-minimal`. This is my usage recommendation, but feel free to use one of the other modes.
109
152
 
110
153
  ## MCP Modes & Available Tools
111
154
 
@@ -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;AAEpD;;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,CAiErE;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,CAyG5D"}
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"}
@@ -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
- currentTextBlock += `\nImage: ${imageFileName} - ${item.image.url}`;
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: item.text || imageFileName,
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"space-resources.d.ts","sourceRoot":"","sources":["../../src/resources/space-resources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAiBtF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAuFvD"}
1
+ {"version":3,"file":"space-resources.d.ts","sourceRoot":"","sources":["../../src/resources/space-resources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAiBtF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAwFvD"}
@@ -35,6 +35,7 @@ function registerSpaceResources(server) {
35
35
  resources: activeSpaces.map((space) => ({
36
36
  uri: `clickup://space/${space.id}`,
37
37
  name: space.name,
38
+ title: space.name,
38
39
  mimeType: "text/plain"
39
40
  }))
40
41
  };
@@ -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;AAIzD;;;;;;;;;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,CAwBhM"}
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
- else {
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
+ }
@@ -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;AAGjE,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;CACb;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
+ {"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,QAwXtE"}
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,12 @@ 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
+ }
123
132
  // Handle tags separately since they need individual API calls
124
133
  let tagUpdateResults = [];
125
134
  if (tags !== undefined) {
@@ -182,8 +191,8 @@ function registerTaskToolsWrite(server, userData) {
182
191
  if (assignees !== undefined) {
183
192
  updateBody.assignees = { add: assignees, rem: [] }; // Add new assignees, remove none
184
193
  }
185
- // Check if there's anything to update (including tags which were handled separately)
186
- if (Object.keys(updateBody).length === 0 && tags === undefined) {
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) {
187
196
  return {
188
197
  content: [
189
198
  {
@@ -210,8 +219,8 @@ function registerTaskToolsWrite(server, userData) {
210
219
  }
211
220
  updatedTask = await updateResponse.json();
212
221
  }
213
- // If only tags were updated, fetch the task again to get the updated state
214
- if (tags !== undefined && Object.keys(updateBody).length === 0) {
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) {
215
224
  const refreshResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
216
225
  headers: { Authorization: config_1.CONFIG.apiKey },
217
226
  });
@@ -220,8 +229,12 @@ function registerTaskToolsWrite(server, userData) {
220
229
  }
221
230
  }
222
231
  const responseLines = formatTaskResponse(updatedTask, 'updated', {
223
- 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
224
233
  }, userData);
234
+ // Add dependency update results if any
235
+ if (dependencyUpdateResults.length > 0) {
236
+ responseLines.push('dependency_warnings: ' + dependencyUpdateResults.join('; '));
237
+ }
225
238
  // Add tag update results if any
226
239
  if (tagUpdateResults.length > 0) {
227
240
  responseLines.push('tag_warnings: ' + tagUpdateResults.join('; '));
@@ -400,6 +413,102 @@ function buildTaskRequestBody(params, currentUserId) {
400
413
  }
401
414
  return requestBody;
402
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
+ }
403
512
  function formatTaskResponse(task, operation, params, userData) {
404
513
  const responseLines = [
405
514
  `Task ${operation} successfully!`,
@@ -434,5 +543,14 @@ function formatTaskResponse(task, operation, params, userData) {
434
543
  if (params.parent_task_id !== undefined) {
435
544
  responseLines.push(`parent_task_id: ${params.parent_task_id}`);
436
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
+ }
437
555
  return responseLines;
438
556
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
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",
@@ -12,12 +12,13 @@
12
12
  "scripts": {
13
13
  "build": "tsc",
14
14
  "start": "node dist/index.js",
15
- "dev": "tsc -w & nodemon dist/index.js",
15
+ "dev": "npx tsc -w & nodemon dist/index.js",
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
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"
20
+ "mcpb": "npm run build && mcpb pack . ClickUp.mcpb",
21
+ "test": "node --test -r ts-node/register src/**/*.test.ts"
21
22
  },
22
23
  "keywords": [
23
24
  "clickup",
@@ -44,11 +45,14 @@
44
45
  "zod": "^3.24.2"
45
46
  },
46
47
  "devDependencies": {
48
+ "@anthropic-ai/mcpb": "^1.1.1",
47
49
  "@types/node": "^22.14.1",
48
50
  "dotenv": "^16.5.0",
49
51
  "nodemon": "^3.1.9",
50
52
  "prettier": "^3.5.3",
51
- "typescript": "^5.8.3"
53
+ "ts-node": "^10.9.2",
54
+ "typescript": "^5.8.3",
55
+ "undici": "^7.16.0"
52
56
  },
53
57
  "engines": {
54
58
  "node": ">=16.0.0"