@hauptsache.net/clickup-mcp 1.1.1 → 1.3.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -21
  3. package/dist/cli.js +8 -0
  4. package/dist/clickup-text.d.ts +5 -2
  5. package/dist/clickup-text.d.ts.map +1 -1
  6. package/dist/clickup-text.js +133 -61
  7. package/dist/index.d.ts +1 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +37 -13
  10. package/dist/shared/config.d.ts +1 -0
  11. package/dist/shared/config.d.ts.map +1 -1
  12. package/dist/shared/config.js +1 -0
  13. package/dist/shared/image-processing.d.ts +13 -0
  14. package/dist/shared/image-processing.d.ts.map +1 -0
  15. package/dist/shared/image-processing.js +126 -0
  16. package/dist/shared/types.d.ts +7 -1
  17. package/dist/shared/types.d.ts.map +1 -1
  18. package/dist/shared/utils.d.ts +24 -26
  19. package/dist/shared/utils.d.ts.map +1 -1
  20. package/dist/shared/utils.js +424 -101
  21. package/dist/tools/doc-tools.d.ts +4 -0
  22. package/dist/tools/doc-tools.d.ts.map +1 -0
  23. package/dist/tools/doc-tools.js +485 -0
  24. package/dist/tools/list-tools.d.ts.map +1 -1
  25. package/dist/tools/list-tools.js +26 -149
  26. package/dist/tools/search-tools.d.ts +1 -1
  27. package/dist/tools/search-tools.d.ts.map +1 -1
  28. package/dist/tools/search-tools.js +85 -62
  29. package/dist/tools/space-tools.d.ts.map +1 -1
  30. package/dist/tools/space-tools.js +153 -27
  31. package/dist/tools/task-tools.d.ts +1 -2
  32. package/dist/tools/task-tools.d.ts.map +1 -1
  33. package/dist/tools/task-tools.js +86 -405
  34. package/dist/tools/task-write-tools.d.ts +3 -0
  35. package/dist/tools/task-write-tools.d.ts.map +1 -0
  36. package/dist/tools/task-write-tools.js +375 -0
  37. package/dist/tools/time-tools.d.ts.map +1 -1
  38. package/dist/tools/time-tools.js +2 -30
  39. package/package.json +15 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marco Pfeiffer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,23 +1,10 @@
1
1
  # ClickUp MCP for AI Assistants
2
2
 
3
- Transform your AI assistant into a powerful ClickUp integration for both **agentic coding** and **productivity management**. This Model Context Protocol (MCP) server enables Claude, Windsurf, Cursor, and other AI assistants to seamlessly interact with your ClickUp workspace.
3
+ Model Context Protocol (MCP) server enabling AI assistants to interact with ClickUp workspaces. Get complete task context with comments and images, search across projects, create and update tasks, collaborate through comments, and track time - all through natural language.
4
4
 
5
5
  > **⚠️ Experimental Software Notice**
6
6
  > This MCP is experimental and under active development. While implemented with great care to prevent data loss through append-only description updates and other safety measures, it cannot be guaranteed to be completely safe. Please use with caution, especially in production environments. Always test in a non-critical workspace first. 🧪
7
7
 
8
- ## Two Powerful Use Cases
9
-
10
- ### 🛠️ **For Developers & Agentic Coding**
11
- Originally built to supercharge coding sessions by providing complete task context to AI coding assistants:
12
-
13
- - **Instant Requirements**: *"Get task CU-abc123"* → AI gets full requirements, acceptance criteria, comments, and images
14
- - **Complete Context**: Pass entire task histories to Claude Code, Windsurf, or Cursor for informed development
15
- - **Visual Specifications**: Include embedded wireframes, mockups, and screenshots in your coding context
16
- - **Implementation History**: Access all previous discussions and decisions for better code alignment
17
-
18
- ### 📋 **For Project Management & Productivity**
19
- Extended capabilities for conversational ClickUp management and daily workflow optimization.
20
-
21
8
  ## What You Can Do
22
9
 
23
10
  Turn natural language into powerful ClickUp actions:
@@ -49,6 +36,14 @@ Turn natural language into powerful ClickUp actions:
49
36
  - *"What's the latest update on the database migration?"*
50
37
  - *"Add a comment to the design task about the new wireframes"*
51
38
 
39
+ **Document Management:**
40
+ - *"Find documents about job posting in hauptsache.net space"*
41
+ - *"Search for API documentation across all spaces"*
42
+ - *"Read the API documentation in the development space"*
43
+ - *"Create a new requirements document for the mobile app project"*
44
+ - *"Update the meeting notes with today's decisions"*
45
+ - *"What documents are in the product strategy space?"*
46
+
52
47
  ## Key Features
53
48
 
54
49
  ### 🔍 **Intelligent Search**
@@ -60,6 +55,7 @@ Turn natural language into powerful ClickUp actions:
60
55
  - Full comment histories and team discussions
61
56
  - Task descriptions with embedded images
62
57
  - List descriptions and project guidelines
58
+ - Document content with page navigation
63
59
  - Access to complete task history and decisions
64
60
 
65
61
  ### ⏱️ **Time Tracking**
@@ -67,8 +63,9 @@ Turn natural language into powerful ClickUp actions:
67
63
  - View historical time logs and entries
68
64
  - Query time entries by task or date range
69
65
 
70
- ### 📋 **Task Management**
66
+ ### 📋 **Task & Document Management**
71
67
  - Create and update tasks with markdown descriptions
68
+ - Create, read, and update documents and pages
72
69
  - Add comments and collaborate with team members
73
70
  - Manage priorities, due dates, assignees, and tags
74
71
  - Handle time estimates and custom field values
@@ -92,8 +89,7 @@ Turn natural language into powerful ClickUp actions:
92
89
  "clickup": {
93
90
  "command": "npx",
94
91
  "args": [
95
- "-y",
96
- "@hauptsache.net/clickup-mcp@1"
92
+ "@hauptsache.net/clickup-mcp@latest"
97
93
  ],
98
94
  "env": {
99
95
  "CLICKUP_API_KEY": "your_api_key",
@@ -123,15 +119,17 @@ The ClickUp MCP supports three operational modes to balance functionality, secur
123
119
  |------|:------------:|:----:|:-----:|-------------|
124
120
  | `getTaskById` | ✅ | ✅ | ✅ | Get complete task details including comments, images, and metadata |
125
121
  | `searchTasks` | ✅ | ✅ | ✅ | Find tasks by content, keywords, assignees, or project context |
126
- | `listSpaces` | ❌ | ✅ | ✅ | Browse workspace structure and project organization |
127
- | `listLists` | ❌ | ✅ | ✅ | Browse lists and folders within spaces |
122
+ | `searchSpaces` | ❌ | ✅ | ✅ | Browse workspace structure, project organization, and documents |
128
123
  | `getListInfo` | ❌ | ✅ | ✅ | Get list details and available statuses for task creation |
129
124
  | `getTimeEntries` | ❌ | ✅ | ✅ | View time entries and analyze time spent across projects |
125
+ | `readDocument` | ❌ | ✅ | ✅ | Get document details, page structure, and content with navigation |
126
+ | `searchDocuments` | ❌ | ✅ | ✅ | Search documents by name and space with fuzzy matching and space filtering |
130
127
  | `createTask` | ❌ | ❌ | ✅ | Create new tasks with full markdown support |
131
128
  | `updateTask` | ❌ | ❌ | ✅ | Update tasks (status, priority, assignees, etc.) with **SAFE APPEND-ONLY** descriptions |
132
129
  | `updateListInfo` | ❌ | ❌ | ✅ | **SAFE APPEND-ONLY** updates to list descriptions (preserves existing content) |
133
130
  | `addComment` | ❌ | ❌ | ✅ | Add comments to tasks for collaboration |
134
131
  | `createTimeEntry` | ❌ | ❌ | ✅ | Log time entries for task tracking |
132
+ | `writeDocument` | ❌ | ❌ | ✅ | Universal document and page operations with smart document creation |
135
133
 
136
134
  ### Setting the Mode
137
135
 
@@ -161,6 +159,7 @@ This MCP server can be configured using environment variables:
161
159
  - `CLICKUP_TEAM_ID`: (Required) Your ClickUp Team ID (formerly Workspace ID).
162
160
  - `CLICKUP_MCP_MODE`: (Optional) Controls which tools are available. Options: `read-minimal`, `read`, `write` (default).
163
161
  - `MAX_IMAGES`: (Optional) The maximum number of images to return for a task in `getTaskById`. Defaults to 4.
162
+ - `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
163
  - `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
164
  - `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
165
 
@@ -219,7 +218,7 @@ This ensures no existing content is ever lost while maintaining a clear audit tr
219
218
  ## Performance & Limitations
220
219
 
221
220
  **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`)
221
+ - **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
222
  - **Search Scope**: Searches within the most recent 1000-3000 tasks to prevent running into rate limits (exact number varies by endpoint)
224
223
  - **Search Results**: Returns up to 50 most relevant matches to prevent flooding the agent with too many results
225
224
 
@@ -232,4 +231,4 @@ These limitations ensure reliable performance while covering the most common use
232
231
 
233
232
  ## License
234
233
 
235
- ISC
234
+ MIT
package/dist/cli.js CHANGED
@@ -7,8 +7,15 @@ async function main() {
7
7
  // Wait for server initialization to complete
8
8
  const server = await index_1.serverPromise;
9
9
  const args = process.argv.slice(2);
10
+ // Special command to show instructions
11
+ if (args.length === 1 && args[0] === 'instructions') {
12
+ console.log("Server Instructions:");
13
+ console.log(server.server._instructions || "No instructions configured");
14
+ process.exit(0);
15
+ }
10
16
  if (args.length < 1) {
11
17
  console.error("Usage: npm run cli <tool-name> [param1=value1 param2=value2 ...]");
18
+ console.error(" npm run cli instructions");
12
19
  console.error("\nAvailable tools:");
13
20
  // @ts-ignore - Accessing private property for testing purposes
14
21
  const tools = server._registeredTools;
@@ -90,6 +97,7 @@ async function main() {
90
97
  // Call the tool's callback function
91
98
  const result = await tool.callback(params);
92
99
  console.dir(result.content);
100
+ process.exit(0);
93
101
  }
94
102
  catch (error) {
95
103
  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[] | null | undefined): (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,GAAG,IAAI,GAAG,SAAS,GAClD,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CAyG5D"}
@@ -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,14 +92,14 @@ 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();
66
- for (const attachment of attachments) {
67
- if (typeof attachment.thumbnail_large === "string") {
68
- thumbnailMap.set(attachment.url, attachment.thumbnail_large);
98
+ // Create a map of attachment URLs to their full info for easy lookup
99
+ const attachmentMap = new Map();
100
+ if (attachments && Array.isArray(attachments)) {
101
+ for (const attachment of attachments) {
102
+ attachmentMap.set(attachment.url, attachment);
69
103
  }
70
104
  }
71
105
  // Regular expression to match markdown image syntax: ![alt text](url)
@@ -77,59 +111,97 @@ async function processClickUpMarkdown(markdownText, attachments) {
77
111
  // Add text before the image reference to the current text block
78
112
  currentTextBlock += markdownText.substring(lastIndex, match.index);
79
113
  // 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
- }));
114
+ const attachment = attachmentMap.get(imageUrl);
115
+ if (attachment) {
116
+ // Add image URL reference inline to current text block
117
+ const imageFileName = altText || "image";
118
+ currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
119
+ // Only create image_metadata if we have at least one thumbnail (never use original image)
120
+ if (attachment.thumbnail_large || attachment.thumbnail_medium || attachment.thumbnail_small) {
121
+ // Push accumulated text (including image URL) as a text block
122
+ if (currentTextBlock.trim()) {
123
+ contentBlocks.push({
124
+ type: "text",
125
+ text: currentTextBlock.trim(),
126
+ });
127
+ }
128
+ // Reset current text block after pushing it
129
+ currentTextBlock = "";
130
+ // Create URLs array with largest to smallest preference, filter out undefined
131
+ const urls = [attachment.thumbnail_large, attachment.thumbnail_medium, attachment.thumbnail_small].filter(Boolean);
132
+ // Add image_metadata block for lazy loading
133
+ contentBlocks.push({
134
+ type: "image_metadata",
135
+ urls: urls,
136
+ alt: altText || imageFileName,
137
+ });
88
138
  }
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
- }));
139
+ // If no thumbnails, just treat as a file reference (already added to currentTextBlock)
96
140
  }
97
141
  else {
98
142
  // If the image URL doesn't match any attachment, keep the original markdown in the current text block
99
143
  currentTextBlock += fullMatch;
100
- console.error(`Image URL ${imageUrl} not found in attachments`, thumbnailMap);
144
+ console.error(`Image URL ${imageUrl} not found in attachments`, attachmentMap);
101
145
  }
102
146
  lastIndex = match.index + fullMatch.length;
103
147
  }
104
148
  // Add any remaining text after the last image
105
149
  currentTextBlock += markdownText.substring(lastIndex);
150
+ // Process non-image attachments that weren't referenced in markdown
151
+ const referencedUrls = new Set();
152
+ const imageMatches = markdownText.matchAll(/!\[([^\]]*)\]\(([^\)]+)\)/g);
153
+ for (const match of imageMatches) {
154
+ referencedUrls.add(match[2]);
155
+ }
156
+ // Add non-image files inline to the current text block
157
+ if (attachments && Array.isArray(attachments)) {
158
+ for (const attachment of attachments) {
159
+ if (!referencedUrls.has(attachment.url)) {
160
+ // Determine if this is an image based on URL or type
161
+ const isImage = attachment.thumbnail_large ||
162
+ /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(attachment.url);
163
+ if (!isImage) {
164
+ // This is a non-image file - add inline to current text block
165
+ const fileName = extractFileNameFromUrl(attachment.url) || "file";
166
+ const fileType = extractFileTypeFromUrl(attachment.url);
167
+ const fileTypeText = fileType ? ` (${fileType.toUpperCase()})` : "";
168
+ currentTextBlock += `\nFile: ${fileName}${fileTypeText} - ${attachment.url}`;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ // Add any remaining text (including file references) as final text block
106
174
  if (currentTextBlock.trim()) {
107
- contentBlocks.push(Promise.resolve({
175
+ contentBlocks.push({
108
176
  type: "text",
109
177
  text: currentTextBlock.trim(),
110
- }));
178
+ });
111
179
  }
112
- return Promise.all(contentBlocks);
180
+ return contentBlocks;
113
181
  }
114
182
  /**
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)
183
+ * Extract filename from URL
120
184
  */
121
- async function loadImageContentBlock(url, fallback) {
185
+ function extractFileNameFromUrl(url) {
122
186
  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
- };
187
+ const urlObj = new URL(url);
188
+ const pathname = urlObj.pathname;
189
+ const filename = pathname.split('/').pop();
190
+ return filename && filename !== '' ? filename : null;
130
191
  }
131
- catch (error) {
132
- console.error(`Error fetching image: ${error.message || "Unknown error"}`);
133
- return fallback;
192
+ catch {
193
+ return null;
134
194
  }
135
195
  }
196
+ /**
197
+ * Extract file extension from URL
198
+ */
199
+ function extractFileTypeFromUrl(url) {
200
+ const filename = extractFileNameFromUrl(url);
201
+ if (!filename)
202
+ return null;
203
+ const lastDot = filename.lastIndexOf('.');
204
+ if (lastDot === -1)
205
+ return null;
206
+ return filename.substring(lastDot + 1);
207
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- declare const server: McpServer;
4
3
  declare const serverPromise: Promise<McpServer>;
5
- export { server, serverPromise };
4
+ export { serverPromise };
6
5
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAkFpE,QAAA,MAAM,aAAa,oBAAqB,CAAC;AAIzC,OAAO,EAAE,aAAa,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -1,53 +1,77 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.serverPromise = exports.server = void 0;
4
+ exports.serverPromise = void 0;
5
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
6
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
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");
14
15
  const time_tools_1 = require("./tools/time-tools");
15
- // Create an MCP server
16
- const server = new mcp_js_1.McpServer({
17
- name: "Clickup MCP",
18
- version: "1.0.0",
19
- });
20
- exports.server = server;
16
+ const doc_tools_1 = require("./tools/doc-tools");
17
+ // Create server variable that will be initialized later
18
+ let server;
21
19
  // Register tools based on mode with user data for enhanced documentation
22
20
  async function initializeServer() {
23
21
  console.error(`Starting ClickUp MCP in ${config_1.CONFIG.mode} mode`);
24
- // Fetch current user for enhanced tool documentation and API health check
25
- const userData = await (0, utils_1.getCurrentUser)();
22
+ // Fetch current user and spaces for enhanced tool documentation and API health check
23
+ const [userData, spacesIndex] = await Promise.all([
24
+ (0, utils_1.getCurrentUser)(),
25
+ (0, utils_1.getSpaceSearchIndex)()
26
+ ]);
27
+ const spaces = spacesIndex._docs || [];
26
28
  console.error(`Connected as: ${userData.user.username} (${userData.user.email})`);
29
+ // Filter out archived spaces and format as simple list
30
+ const activeSpaces = spaces.filter((s) => !s.archived);
31
+ const formattedSpaces = activeSpaces
32
+ .map((s) => `- ${s.name} (space_id: ${s.id})`)
33
+ .join('\n');
34
+ const instructions = [
35
+ `ClickUp is a Ticket system. It is used to track tasks, bugs, and other work items.`,
36
+ `Is you are asked for infos about projects or tasks, search for tasks or documents in ClickUp (this MCP) first.`,
37
+ `The following spaces/projects are available:`,
38
+ formattedSpaces
39
+ ].join('\n');
40
+ console.error(`Pre-loaded ${activeSpaces.length} active spaces`);
41
+ // Create the MCP server with instructions
42
+ server = new mcp_js_1.McpServer({
43
+ name: "Clickup MCP",
44
+ version: require('../package.json').version,
45
+ }, {
46
+ instructions
47
+ });
27
48
  if (config_1.CONFIG.mode === 'read-minimal') {
28
49
  // Core task context tools for AI coding assistance
29
50
  // Only getTaskById and searchTasks
30
51
  (0, task_tools_1.registerTaskToolsRead)(server, userData);
31
- (0, search_tools_1.registerSearchTools)(server);
52
+ (0, search_tools_1.registerSearchTools)(server, userData);
32
53
  }
33
54
  else if (config_1.CONFIG.mode === 'read') {
34
55
  // All read-only tools
35
56
  (0, task_tools_1.registerTaskToolsRead)(server, userData);
36
- (0, search_tools_1.registerSearchTools)(server);
57
+ (0, search_tools_1.registerSearchTools)(server, userData);
37
58
  (0, space_tools_1.registerSpaceTools)(server);
38
59
  (0, list_tools_1.registerListToolsRead)(server);
39
60
  (0, time_tools_1.registerTimeToolsRead)(server);
61
+ (0, doc_tools_1.registerDocumentToolsRead)(server);
40
62
  }
41
63
  else if (config_1.CONFIG.mode === 'write') {
42
64
  // All tools (full functionality)
43
65
  (0, task_tools_1.registerTaskToolsRead)(server, userData);
44
- (0, task_tools_1.registerTaskToolsWrite)(server, userData);
45
- (0, search_tools_1.registerSearchTools)(server);
66
+ (0, task_write_tools_1.registerTaskToolsWrite)(server, userData);
67
+ (0, search_tools_1.registerSearchTools)(server, userData);
46
68
  (0, space_tools_1.registerSpaceTools)(server);
47
69
  (0, list_tools_1.registerListToolsRead)(server);
48
70
  (0, list_tools_1.registerListToolsWrite)(server);
49
71
  (0, time_tools_1.registerTimeToolsRead)(server);
50
72
  (0, time_tools_1.registerTimeToolsWrite)(server);
73
+ (0, doc_tools_1.registerDocumentToolsRead)(server);
74
+ (0, doc_tools_1.registerDocumentToolsWrite)(server);
51
75
  }
52
76
  return server;
53
77
  }
@@ -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"}