@hauptsache.net/clickup-mcp 1.1.0 → 1.2.0
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 +6 -1
- package/dist/cli.js +1 -0
- package/dist/clickup-text.d.ts +5 -2
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +129 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/shared/config.d.ts +1 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +1 -0
- package/dist/shared/image-processing.d.ts +13 -0
- package/dist/shared/image-processing.d.ts.map +1 -0
- package/dist/shared/image-processing.js +126 -0
- package/dist/shared/types.d.ts +7 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/utils.d.ts +62 -11
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +281 -63
- package/dist/tools/list-tools.d.ts.map +1 -1
- package/dist/tools/list-tools.js +34 -143
- package/dist/tools/search-tools.d.ts +1 -1
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +79 -33
- package/dist/tools/space-tools.d.ts.map +1 -1
- package/dist/tools/space-tools.js +160 -31
- package/dist/tools/task-tools.d.ts +1 -2
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +92 -380
- package/dist/tools/task-write-tools.d.ts +3 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -0
- package/dist/tools/task-write-tools.js +375 -0
- package/dist/tools/time-tools.d.ts.map +1 -1
- package/dist/tools/time-tools.js +191 -111
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -157,10 +157,15 @@ Add the mode to your MCP configuration:
|
|
|
157
157
|
|
|
158
158
|
This MCP server can be configured using environment variables:
|
|
159
159
|
|
|
160
|
+
### ⚠️ Breaking Changes for v1.2.0+
|
|
161
|
+
- **Image Response Size Limiting**: A new `MAX_RESPONSE_SIZE_MB` environment variable (default: 1MB) may cause existing tasks with large images to display fewer images than before. To restore previous behavior, set `MAX_RESPONSE_SIZE_MB=10` (or higher) in your environment.
|
|
162
|
+
- **Enhanced API Rate Limiting**: The search indexing now uses more conservative API call patterns to prevent rate limit exhaustion. Large workspaces may experience slightly slower initial search index building.
|
|
163
|
+
|
|
160
164
|
- `CLICKUP_API_KEY`: (Required) Your ClickUp API key.
|
|
161
165
|
- `CLICKUP_TEAM_ID`: (Required) Your ClickUp Team ID (formerly Workspace ID).
|
|
162
166
|
- `CLICKUP_MCP_MODE`: (Optional) Controls which tools are available. Options: `read-minimal`, `read`, `write` (default).
|
|
163
167
|
- `MAX_IMAGES`: (Optional) The maximum number of images to return for a task in `getTaskById`. Defaults to 4.
|
|
168
|
+
- `MAX_RESPONSE_SIZE_MB`: (Optional) The maximum response size in megabytes for `getTaskById`. Uses intelligent size budgeting to fit the most important images within the limit. Defaults to 1.
|
|
164
169
|
- `CLICKUP_PRIMARY_LANGUAGE`: (Optional) A hint for the primary language used in your ClickUp tasks (e.g., "de" for German, "en" for English). This helps the `searchTask` tool provide more tailored guidance in its description for multilingual searches.
|
|
165
170
|
- `LANG`: (Optional) If `CLICKUP_PRIMARY_LANGUAGE` is not set, the MCP will check this standard environment variable (e.g., "en_US.UTF-8", "de_DE") as a fallback to infer the primary language.
|
|
166
171
|
|
|
@@ -219,7 +224,7 @@ This ensures no existing content is ever lost while maintaining a clear audit tr
|
|
|
219
224
|
## Performance & Limitations
|
|
220
225
|
|
|
221
226
|
**Optimized for AI Workflows:**
|
|
222
|
-
- **Image Processing**:
|
|
227
|
+
- **Smart Image Processing**: Intelligent size budgeting prioritizes the most recent images while respecting both count (`MAX_IMAGES`, default: 4) and total response size limits (`MAX_RESPONSE_SIZE_MB`, default: 1MB)
|
|
223
228
|
- **Search Scope**: Searches within the most recent 1000-3000 tasks to prevent running into rate limits (exact number varies by endpoint)
|
|
224
229
|
- **Search Results**: Returns up to 50 most relevant matches to prevent flooding the agent with too many results
|
|
225
230
|
|
package/dist/cli.js
CHANGED
package/dist/clickup-text.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
|
+
import { ImageMetadataBlock } from "./shared/types";
|
|
2
3
|
/**
|
|
3
4
|
* Represents a ClickUp text item which can be plain text or an image
|
|
4
5
|
*/
|
|
@@ -24,6 +25,8 @@ export interface ClickUpTextItem {
|
|
|
24
25
|
*/
|
|
25
26
|
export interface ClickUpAttachment {
|
|
26
27
|
thumbnail_large?: string;
|
|
28
|
+
thumbnail_medium?: string;
|
|
29
|
+
thumbnail_small?: string;
|
|
27
30
|
url: string;
|
|
28
31
|
[key: string]: any;
|
|
29
32
|
}
|
|
@@ -34,12 +37,12 @@ export interface ClickUpAttachment {
|
|
|
34
37
|
* @param textItems Array of text items from ClickUp API
|
|
35
38
|
* @returns Promise resolving to an array of content blocks (text and images)
|
|
36
39
|
*/
|
|
37
|
-
export declare function processClickUpText(textItems: ClickUpTextItem[]): Promise<CallToolResult["content"]>;
|
|
40
|
+
export declare function processClickUpText(textItems: ClickUpTextItem[]): Promise<(CallToolResult["content"][number] | ImageMetadataBlock)[]>;
|
|
38
41
|
/**
|
|
39
42
|
* Splits markdown text at image references and converts them to image blocks
|
|
40
43
|
* @param markdownText The markdown text to process
|
|
41
44
|
* @param attachments Array of attachments from the Clickup API
|
|
42
45
|
* @returns Array of content blocks (text and images)
|
|
43
46
|
*/
|
|
44
|
-
export declare function processClickUpMarkdown(markdownText: string, attachments: ClickUpAttachment[]):
|
|
47
|
+
export declare function processClickUpMarkdown(markdownText: string, attachments: ClickUpAttachment[]): (CallToolResult["content"][number] | ImageMetadataBlock)[];
|
|
45
48
|
//# sourceMappingURL=clickup-text.d.ts.map
|
|
@@ -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;
|
|
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,GAC/B,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CAqG5D"}
|
package/dist/clickup-text.js
CHANGED
|
@@ -2,7 +2,27 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.processClickUpText = processClickUpText;
|
|
4
4
|
exports.processClickUpMarkdown = processClickUpMarkdown;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Extract thumbnail URLs from data-attachment attribute JSON
|
|
7
|
+
* ClickUp API sometimes has broken thumbnail URLs, but data-attachment contains working ones
|
|
8
|
+
*/
|
|
9
|
+
function extractThumbnailsFromDataAttachment(attributes) {
|
|
10
|
+
if (!attributes || !attributes['data-attachment']) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const attachmentData = JSON.parse(attributes['data-attachment']);
|
|
15
|
+
return {
|
|
16
|
+
thumbnail_large: attachmentData.thumbnail_large,
|
|
17
|
+
thumbnail_medium: attachmentData.thumbnail_medium,
|
|
18
|
+
thumbnail_small: attachmentData.thumbnail_small,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error('Error parsing data-attachment:', error);
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Process an array of ClickUp text items into a structured content format
|
|
8
28
|
* that includes both text and images in their original sequence
|
|
@@ -16,23 +36,37 @@ async function processClickUpText(textItems) {
|
|
|
16
36
|
for (let i = 0; i < textItems.length; i++) {
|
|
17
37
|
const item = textItems[i];
|
|
18
38
|
// Handle image items
|
|
19
|
-
if (item.type === "image" && item.image && item.image.
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
if (item.type === "image" && item.image && item.image.url) {
|
|
40
|
+
// Add image URL reference inline to current text block
|
|
41
|
+
const imageFileName = item.image.name || item.image.title || "image";
|
|
42
|
+
currentTextBlock += `\nImage: ${imageFileName} - ${item.image.url}`;
|
|
43
|
+
// Get working thumbnail URLs from data-attachment if available
|
|
44
|
+
const extractedThumbnails = extractThumbnailsFromDataAttachment(item.attributes);
|
|
45
|
+
// Determine best thumbnail URLs (prefer extracted over API thumbnails)
|
|
46
|
+
const thumbnail_large = extractedThumbnails.thumbnail_large || item.image.thumbnail_large;
|
|
47
|
+
const thumbnail_medium = extractedThumbnails.thumbnail_medium || item.image.thumbnail_medium;
|
|
48
|
+
const thumbnail_small = extractedThumbnails.thumbnail_small || item.image.thumbnail_small;
|
|
49
|
+
// Only create image_metadata if we have at least one thumbnail (never use original image)
|
|
50
|
+
if (thumbnail_large || thumbnail_medium || thumbnail_small) {
|
|
51
|
+
// Push accumulated text (including image URL) as a text block
|
|
52
|
+
if (currentTextBlock.trim()) {
|
|
53
|
+
contentBlocks.push({
|
|
54
|
+
type: "text",
|
|
55
|
+
text: currentTextBlock.trim(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Reset current text block after pushing it
|
|
59
|
+
currentTextBlock = "";
|
|
60
|
+
// Create URLs array with largest to smallest preference, filter out undefined
|
|
61
|
+
const urls = [thumbnail_large, thumbnail_medium, thumbnail_small].filter(Boolean);
|
|
62
|
+
// Add image_metadata block for lazy loading
|
|
63
|
+
contentBlocks.push({
|
|
64
|
+
type: "image_metadata",
|
|
65
|
+
urls: urls,
|
|
66
|
+
alt: item.text || imageFileName,
|
|
67
|
+
});
|
|
35
68
|
}
|
|
69
|
+
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
|
|
36
70
|
}
|
|
37
71
|
// Handle text items
|
|
38
72
|
else if (typeof item.text === "string") {
|
|
@@ -45,12 +79,12 @@ async function processClickUpText(textItems) {
|
|
|
45
79
|
}
|
|
46
80
|
// Add any remaining text
|
|
47
81
|
if (currentTextBlock.trim()) {
|
|
48
|
-
contentBlocks.push(
|
|
82
|
+
contentBlocks.push({
|
|
49
83
|
type: "text",
|
|
50
84
|
text: currentTextBlock.trim(),
|
|
51
|
-
})
|
|
85
|
+
});
|
|
52
86
|
}
|
|
53
|
-
return
|
|
87
|
+
return contentBlocks;
|
|
54
88
|
}
|
|
55
89
|
/**
|
|
56
90
|
* Splits markdown text at image references and converts them to image blocks
|
|
@@ -58,15 +92,13 @@ async function processClickUpText(textItems) {
|
|
|
58
92
|
* @param attachments Array of attachments from the Clickup API
|
|
59
93
|
* @returns Array of content blocks (text and images)
|
|
60
94
|
*/
|
|
61
|
-
|
|
95
|
+
function processClickUpMarkdown(markdownText, attachments) {
|
|
62
96
|
const contentBlocks = [];
|
|
63
97
|
let currentTextBlock = "";
|
|
64
|
-
// Create a map of attachment URLs to their
|
|
65
|
-
const
|
|
98
|
+
// Create a map of attachment URLs to their full info for easy lookup
|
|
99
|
+
const attachmentMap = new Map();
|
|
66
100
|
for (const attachment of attachments) {
|
|
67
|
-
|
|
68
|
-
thumbnailMap.set(attachment.url, attachment.thumbnail_large);
|
|
69
|
-
}
|
|
101
|
+
attachmentMap.set(attachment.url, attachment);
|
|
70
102
|
}
|
|
71
103
|
// Regular expression to match markdown image syntax: 
|
|
72
104
|
const imageRegex = /!\[([^\]]*)\]\(([^\)]+)\)/g;
|
|
@@ -77,59 +109,95 @@ async function processClickUpMarkdown(markdownText, attachments) {
|
|
|
77
109
|
// Add text before the image reference to the current text block
|
|
78
110
|
currentTextBlock += markdownText.substring(lastIndex, match.index);
|
|
79
111
|
// Check if this image URL exists in our attachments
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
const attachment = attachmentMap.get(imageUrl);
|
|
113
|
+
if (attachment) {
|
|
114
|
+
// Add image URL reference inline to current text block
|
|
115
|
+
const imageFileName = altText || "image";
|
|
116
|
+
currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
|
|
117
|
+
// Only create image_metadata if we have at least one thumbnail (never use original image)
|
|
118
|
+
if (attachment.thumbnail_large || attachment.thumbnail_medium || attachment.thumbnail_small) {
|
|
119
|
+
// Push accumulated text (including image URL) as a text block
|
|
120
|
+
if (currentTextBlock.trim()) {
|
|
121
|
+
contentBlocks.push({
|
|
122
|
+
type: "text",
|
|
123
|
+
text: currentTextBlock.trim(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Reset current text block after pushing it
|
|
127
|
+
currentTextBlock = "";
|
|
128
|
+
// Create URLs array with largest to smallest preference, filter out undefined
|
|
129
|
+
const urls = [attachment.thumbnail_large, attachment.thumbnail_medium, attachment.thumbnail_small].filter(Boolean);
|
|
130
|
+
// Add image_metadata block for lazy loading
|
|
131
|
+
contentBlocks.push({
|
|
132
|
+
type: "image_metadata",
|
|
133
|
+
urls: urls,
|
|
134
|
+
alt: altText || imageFileName,
|
|
135
|
+
});
|
|
88
136
|
}
|
|
89
|
-
//
|
|
90
|
-
currentTextBlock = "";
|
|
91
|
-
// Add this image as an image block
|
|
92
|
-
contentBlocks.push(loadImageContentBlock(thumbnailUrl, {
|
|
93
|
-
type: "text",
|
|
94
|
-
text: fullMatch.trim(),
|
|
95
|
-
}));
|
|
137
|
+
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
|
|
96
138
|
}
|
|
97
139
|
else {
|
|
98
140
|
// If the image URL doesn't match any attachment, keep the original markdown in the current text block
|
|
99
141
|
currentTextBlock += fullMatch;
|
|
100
|
-
console.error(`Image URL ${imageUrl} not found in attachments`,
|
|
142
|
+
console.error(`Image URL ${imageUrl} not found in attachments`, attachmentMap);
|
|
101
143
|
}
|
|
102
144
|
lastIndex = match.index + fullMatch.length;
|
|
103
145
|
}
|
|
104
146
|
// Add any remaining text after the last image
|
|
105
147
|
currentTextBlock += markdownText.substring(lastIndex);
|
|
148
|
+
// Process non-image attachments that weren't referenced in markdown
|
|
149
|
+
const referencedUrls = new Set();
|
|
150
|
+
const imageMatches = markdownText.matchAll(/!\[([^\]]*)\]\(([^\)]+)\)/g);
|
|
151
|
+
for (const match of imageMatches) {
|
|
152
|
+
referencedUrls.add(match[2]);
|
|
153
|
+
}
|
|
154
|
+
// Add non-image files inline to the current text block
|
|
155
|
+
for (const attachment of attachments) {
|
|
156
|
+
if (!referencedUrls.has(attachment.url)) {
|
|
157
|
+
// Determine if this is an image based on URL or type
|
|
158
|
+
const isImage = attachment.thumbnail_large ||
|
|
159
|
+
/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(attachment.url);
|
|
160
|
+
if (!isImage) {
|
|
161
|
+
// This is a non-image file - add inline to current text block
|
|
162
|
+
const fileName = extractFileNameFromUrl(attachment.url) || "file";
|
|
163
|
+
const fileType = extractFileTypeFromUrl(attachment.url);
|
|
164
|
+
const fileTypeText = fileType ? ` (${fileType.toUpperCase()})` : "";
|
|
165
|
+
currentTextBlock += `\nFile: ${fileName}${fileTypeText} - ${attachment.url}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Add any remaining text (including file references) as final text block
|
|
106
170
|
if (currentTextBlock.trim()) {
|
|
107
|
-
contentBlocks.push(
|
|
171
|
+
contentBlocks.push({
|
|
108
172
|
type: "text",
|
|
109
173
|
text: currentTextBlock.trim(),
|
|
110
|
-
})
|
|
174
|
+
});
|
|
111
175
|
}
|
|
112
|
-
return
|
|
176
|
+
return contentBlocks;
|
|
113
177
|
}
|
|
114
178
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* @param url URL of the image to load
|
|
118
|
-
* @param fallback Fallback content if image loading fails
|
|
119
|
-
* @returns Promise resolving to a content block (either image or fallback text)
|
|
179
|
+
* Extract filename from URL
|
|
120
180
|
*/
|
|
121
|
-
|
|
181
|
+
function extractFileNameFromUrl(url) {
|
|
122
182
|
try {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
mimeType: response.headers.get("Content-Type") || "image/png",
|
|
128
|
-
data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
|
|
129
|
-
};
|
|
183
|
+
const urlObj = new URL(url);
|
|
184
|
+
const pathname = urlObj.pathname;
|
|
185
|
+
const filename = pathname.split('/').pop();
|
|
186
|
+
return filename && filename !== '' ? filename : null;
|
|
130
187
|
}
|
|
131
|
-
catch
|
|
132
|
-
|
|
133
|
-
return fallback;
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
134
190
|
}
|
|
135
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Extract file extension from URL
|
|
194
|
+
*/
|
|
195
|
+
function extractFileTypeFromUrl(url) {
|
|
196
|
+
const filename = extractFileNameFromUrl(url);
|
|
197
|
+
if (!filename)
|
|
198
|
+
return null;
|
|
199
|
+
const lastDot = filename.lastIndexOf('.');
|
|
200
|
+
if (lastDot === -1)
|
|
201
|
+
return null;
|
|
202
|
+
return filename.substring(lastDot + 1);
|
|
203
|
+
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,QAAA,MAAM,MAAM,WAGV,CAAC;AAsCH,QAAA,MAAM,aAAa,oBAAqB,CAAC;AAGzC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const config_1 = require("./shared/config");
|
|
|
8
8
|
const utils_1 = require("./shared/utils");
|
|
9
9
|
// Import tool registration functions
|
|
10
10
|
const task_tools_1 = require("./tools/task-tools");
|
|
11
|
+
const task_write_tools_1 = require("./tools/task-write-tools");
|
|
11
12
|
const search_tools_1 = require("./tools/search-tools");
|
|
12
13
|
const space_tools_1 = require("./tools/space-tools");
|
|
13
14
|
const list_tools_1 = require("./tools/list-tools");
|
|
@@ -15,7 +16,7 @@ const time_tools_1 = require("./tools/time-tools");
|
|
|
15
16
|
// Create an MCP server
|
|
16
17
|
const server = new mcp_js_1.McpServer({
|
|
17
18
|
name: "Clickup MCP",
|
|
18
|
-
version:
|
|
19
|
+
version: require('../package.json').version,
|
|
19
20
|
});
|
|
20
21
|
exports.server = server;
|
|
21
22
|
// Register tools based on mode with user data for enhanced documentation
|
|
@@ -28,12 +29,12 @@ async function initializeServer() {
|
|
|
28
29
|
// Core task context tools for AI coding assistance
|
|
29
30
|
// Only getTaskById and searchTasks
|
|
30
31
|
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
31
|
-
(0, search_tools_1.registerSearchTools)(server);
|
|
32
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
32
33
|
}
|
|
33
34
|
else if (config_1.CONFIG.mode === 'read') {
|
|
34
35
|
// All read-only tools
|
|
35
36
|
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
36
|
-
(0, search_tools_1.registerSearchTools)(server);
|
|
37
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
37
38
|
(0, space_tools_1.registerSpaceTools)(server);
|
|
38
39
|
(0, list_tools_1.registerListToolsRead)(server);
|
|
39
40
|
(0, time_tools_1.registerTimeToolsRead)(server);
|
|
@@ -41,8 +42,8 @@ async function initializeServer() {
|
|
|
41
42
|
else if (config_1.CONFIG.mode === 'write') {
|
|
42
43
|
// All tools (full functionality)
|
|
43
44
|
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
44
|
-
(0,
|
|
45
|
-
(0, search_tools_1.registerSearchTools)(server);
|
|
45
|
+
(0, task_write_tools_1.registerTaskToolsWrite)(server, userData);
|
|
46
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
46
47
|
(0, space_tools_1.registerSpaceTools)(server);
|
|
47
48
|
(0, list_tools_1.registerListToolsRead)(server);
|
|
48
49
|
(0, list_tools_1.registerListToolsWrite)(server);
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAavF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAavF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
|
package/dist/shared/config.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.CONFIG = {
|
|
|
23
23
|
apiKey: process.env.CLICKUP_API_KEY,
|
|
24
24
|
teamId: process.env.CLICKUP_TEAM_ID,
|
|
25
25
|
maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
|
|
26
|
+
maxResponseSizeMB: process.env.MAX_RESPONSE_SIZE_MB ? parseFloat(process.env.MAX_RESPONSE_SIZE_MB) : 1,
|
|
26
27
|
primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
|
|
27
28
|
mode: mcpMode,
|
|
28
29
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ContentBlock, ImageMetadataBlock } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Downloads images from image_metadata blocks and applies smart size/count limiting
|
|
4
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
5
|
+
* Uses intelligent size calculation accounting for text content
|
|
6
|
+
*
|
|
7
|
+
* @param content Array of content blocks that may contain image_metadata blocks
|
|
8
|
+
* @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
|
|
9
|
+
* @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
|
|
10
|
+
* @returns Promise resolving to content array with downloaded images or placeholders
|
|
11
|
+
*/
|
|
12
|
+
export declare function downloadImages(content: (ContentBlock | ImageMetadataBlock)[], maxImages?: number, maxSizeMB?: number): Promise<ContentBlock[]>;
|
|
13
|
+
//# sourceMappingURL=image-processing.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadImages = downloadImages;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const buffer_1 = require("buffer");
|
|
6
|
+
/**
|
|
7
|
+
* Downloads images from image_metadata blocks and applies smart size/count limiting
|
|
8
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
9
|
+
* Uses intelligent size calculation accounting for text content
|
|
10
|
+
*
|
|
11
|
+
* @param content Array of content blocks that may contain image_metadata blocks
|
|
12
|
+
* @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
|
|
13
|
+
* @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
|
|
14
|
+
* @returns Promise resolving to content array with downloaded images or placeholders
|
|
15
|
+
*/
|
|
16
|
+
async function downloadImages(content, maxImages = config_1.CONFIG.maxImages, maxSizeMB = config_1.CONFIG.maxResponseSizeMB) {
|
|
17
|
+
// First apply count-based limiting to image_metadata blocks
|
|
18
|
+
const countLimitedContent = applyCountBasedLimitToImageMetadata(content, maxImages);
|
|
19
|
+
// Calculate text size to determine available image budget
|
|
20
|
+
const textContent = countLimitedContent.filter(block => block.type !== "image_metadata");
|
|
21
|
+
const textSizeBytes = JSON.stringify(textContent).length;
|
|
22
|
+
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
23
|
+
const availableImageBudget = Math.max(0, maxSizeBytes - textSizeBytes);
|
|
24
|
+
// Calculate per-image budget
|
|
25
|
+
const imageMetadataBlocks = countLimitedContent.filter(block => block.type === "image_metadata");
|
|
26
|
+
const perImageBudget = imageMetadataBlocks.length > 0 ? availableImageBudget / imageMetadataBlocks.length : 0;
|
|
27
|
+
// Download images in parallel and replace image_metadata blocks
|
|
28
|
+
const downloadPromises = countLimitedContent.map(async (block) => {
|
|
29
|
+
if (block.type === "image_metadata") {
|
|
30
|
+
return await downloadSingleImage(block, perImageBudget);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return block;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return Promise.all(downloadPromises);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Apply count-based limiting to image_metadata blocks
|
|
40
|
+
* Keeps the most recent image_metadata blocks (newest last)
|
|
41
|
+
*/
|
|
42
|
+
function applyCountBasedLimitToImageMetadata(content, maxImages) {
|
|
43
|
+
// Find all image_metadata block indices
|
|
44
|
+
const imageMetadataIndices = [];
|
|
45
|
+
content.forEach((block, index) => {
|
|
46
|
+
if (block.type === "image_metadata") {
|
|
47
|
+
imageMetadataIndices.push(index);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// If we have fewer images than the limit, return the original content
|
|
51
|
+
if (imageMetadataIndices.length <= maxImages) {
|
|
52
|
+
return content;
|
|
53
|
+
}
|
|
54
|
+
// Determine which image_metadata blocks to remove (keep the most recent ones)
|
|
55
|
+
const imagesToRemove = imageMetadataIndices.slice(0, imageMetadataIndices.length - maxImages);
|
|
56
|
+
// Create a new content array with excess image_metadata blocks replaced by text placeholders
|
|
57
|
+
return content.map((block, index) => {
|
|
58
|
+
if (block.type === "image_metadata" && imagesToRemove.includes(index)) {
|
|
59
|
+
return {
|
|
60
|
+
type: "text",
|
|
61
|
+
text: "[Image removed due to count limitations. Only the most recent images are shown.]",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return block;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Download a single image from an image_metadata block, trying different sizes if needed
|
|
69
|
+
*/
|
|
70
|
+
async function downloadSingleImage(imageMetadata, perImageBudget) {
|
|
71
|
+
const fallbackText = {
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`,
|
|
74
|
+
};
|
|
75
|
+
// Try each URL in order (largest to smallest)
|
|
76
|
+
for (const url of imageMetadata.urls) {
|
|
77
|
+
const abortController = new AbortController();
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
signal: abortController.signal
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
console.error(`Failed to fetch image from ${url}: ${response.status}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Check Content-Length header first to avoid downloading large images
|
|
87
|
+
const contentLength = response.headers.get("Content-Length");
|
|
88
|
+
if (contentLength) {
|
|
89
|
+
const imageSizeBytes = parseInt(contentLength, 10);
|
|
90
|
+
if (imageSizeBytes > perImageBudget) {
|
|
91
|
+
// Cancel the request to stop any ongoing download
|
|
92
|
+
abortController.abort();
|
|
93
|
+
console.error(`Image from ${url} is ${imageSizeBytes} bytes (from Content-Length), exceeds budget of ${perImageBudget} bytes`);
|
|
94
|
+
// Continue to try smaller thumbnail
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const imageBuffer = await response.arrayBuffer();
|
|
99
|
+
const actualSizeBytes = imageBuffer.byteLength;
|
|
100
|
+
// Double-check actual size (in case Content-Length was missing or incorrect)
|
|
101
|
+
if (actualSizeBytes <= perImageBudget) {
|
|
102
|
+
return {
|
|
103
|
+
type: "image",
|
|
104
|
+
mimeType: response.headers.get("Content-Type") || "image/png",
|
|
105
|
+
data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Cancel to clean up the connection
|
|
110
|
+
abortController.abort();
|
|
111
|
+
console.error(`Image from ${url} is ${actualSizeBytes} bytes (actual size), exceeds budget of ${perImageBudget} bytes`);
|
|
112
|
+
// Continue to try smaller thumbnail
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.name === 'AbortError') {
|
|
117
|
+
// Request was cancelled, this is expected - continue to next URL
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
console.error(`Error fetching image from ${url}: ${error.message || "Unknown error"}`);
|
|
121
|
+
// Continue to try next URL
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// If all URLs failed or were too large, return fallback text
|
|
125
|
+
return fallbackText;
|
|
126
|
+
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
2
|
export type ContentBlock = CallToolResult['content'][number];
|
|
3
|
+
export interface ImageMetadataBlock {
|
|
4
|
+
type: "image_metadata";
|
|
5
|
+
urls: string[];
|
|
6
|
+
alt: string;
|
|
7
|
+
}
|
|
3
8
|
export interface DatedContentEvent {
|
|
4
9
|
date: string;
|
|
5
|
-
contentBlocks: ContentBlock[];
|
|
10
|
+
contentBlocks: (ContentBlock | ImageMetadataBlock)[];
|
|
6
11
|
}
|
|
7
12
|
export interface Config {
|
|
8
13
|
apiKey: string;
|
|
9
14
|
teamId: string;
|
|
10
15
|
maxImages: number;
|
|
16
|
+
maxResponseSizeMB: number;
|
|
11
17
|
primaryLanguageHint: string | undefined;
|
|
12
18
|
}
|
|
13
19
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -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,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;
|
|
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"}
|