@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 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**: Limited to 4 most recent images per task to prevent running into mcp client limitations (configurable via `MAX_IMAGES`)
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
@@ -90,6 +90,7 @@ async function main() {
90
90
  // Call the tool's callback function
91
91
  const result = await tool.callback(params);
92
92
  console.dir(result.content);
93
+ process.exit(0);
93
94
  }
94
95
  catch (error) {
95
96
  if (error instanceof Error) {
@@ -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[]): Promise<CallToolResult["content"]>;
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;AAGjE;;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,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAAE,GAC3B,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAqDpC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,EAAE,GAC/B,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAsEpC"}
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"}
@@ -2,7 +2,27 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processClickUpText = processClickUpText;
4
4
  exports.processClickUpMarkdown = processClickUpMarkdown;
5
- const buffer_1 = require("buffer");
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.thumbnail_large) {
20
- // If we have accumulated text, add it as a text block before adding the image
21
- if (currentTextBlock.trim()) {
22
- contentBlocks.push(Promise.resolve({
23
- type: "text",
24
- text: currentTextBlock.trim(),
25
- }));
26
- }
27
- // Reset current text block after pushing it
28
- currentTextBlock = "";
29
- // Add this image as an image block
30
- if (typeof item.image.thumbnail_large === "string") {
31
- contentBlocks.push(loadImageContentBlock(item.image.thumbnail_large, {
32
- type: "text",
33
- text: item.text || "Image",
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(Promise.resolve({
82
+ contentBlocks.push({
49
83
  type: "text",
50
84
  text: currentTextBlock.trim(),
51
- }));
85
+ });
52
86
  }
53
- return Promise.all(contentBlocks);
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
- async function processClickUpMarkdown(markdownText, attachments) {
95
+ function processClickUpMarkdown(markdownText, attachments) {
62
96
  const contentBlocks = [];
63
97
  let currentTextBlock = "";
64
- // Create a map of attachment URLs to their thumbnail_large URLs for easy lookup
65
- const thumbnailMap = new Map();
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
- if (typeof attachment.thumbnail_large === "string") {
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: ![alt text](url)
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 thumbnailUrl = thumbnailMap.get(imageUrl);
81
- if (thumbnailUrl) {
82
- // If we have accumulated text, add it as a text block before adding the image
83
- if (currentTextBlock.trim()) {
84
- contentBlocks.push(Promise.resolve({
85
- type: "text",
86
- text: currentTextBlock.trim(),
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
- // Reset current text block after pushing it
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`, thumbnailMap);
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(Promise.resolve({
171
+ contentBlocks.push({
108
172
  type: "text",
109
173
  text: currentTextBlock.trim(),
110
- }));
174
+ });
111
175
  }
112
- return Promise.all(contentBlocks);
176
+ return contentBlocks;
113
177
  }
114
178
  /**
115
- * Loads an image from a URL and converts it to a base64-encoded content block
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
- async function loadImageContentBlock(url, fallback) {
181
+ function extractFileNameFromUrl(url) {
122
182
  try {
123
- const response = await fetch(url);
124
- const imageBuffer = await response.arrayBuffer();
125
- return {
126
- type: "image",
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 (error) {
132
- console.error(`Error fetching image: ${error.message || "Unknown error"}`);
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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAapE,QAAA,MAAM,MAAM,WAGV,CAAC;AAsCH,QAAA,MAAM,aAAa,oBAAqB,CAAC;AAGzC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,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: "1.0.0",
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, task_tools_1.registerTaskToolsWrite)(server, userData);
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);
@@ -4,6 +4,7 @@ export declare const CONFIG: {
4
4
  apiKey: string;
5
5
  teamId: string;
6
6
  maxImages: number;
7
+ maxResponseSizeMB: number;
7
8
  primaryLanguageHint: string | undefined;
8
9
  mode: McpMode;
9
10
  };
@@ -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;;;;;;CAMlB,CAAC"}
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"}
@@ -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
+ }
@@ -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;CAC/B;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,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;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"}