@hauptsache.net/clickup-mcp 1.5.1 → 1.6.1

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
@@ -2,6 +2,34 @@
2
2
 
3
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
+ ## This MCP vs Official ClickUp MCP
6
+
7
+ > See also: [Official ClickUp MCP Documentation](https://developer.clickup.com/docs/connect-an-ai-assistant-to-clickups-mcp-server)
8
+
9
+ | Feature | This MCP | Official ClickUp MCP |
10
+ |----------------------|-------------------------------------------------------|---------------------------------------------|
11
+ | **Setup** | Local npm/npx install | Remote MCP (no install) |
12
+ | **Authentication** | API key only | OAuth only |
13
+ | **Task Context** | Complete with comments, status history, inline images | Requires mutiple tool calls for full contxt |
14
+ | **Image Support** | Inline images with smart size budgeting | Not documented |
15
+ | **Search** | Fuzzy search on recent tasks (limited scope) | Full ClickUp search database |
16
+ | **Documents** | CRUD operations | CRUD + document search |
17
+ | **Time Tracking** | View and create entries | Timers and entries |
18
+ | **Chat Integration** | Not supported | Supported |
19
+ | **Connected Apps** | Not supported | Connected Search |
20
+ | **Best For** | Coding tools, automation, context gathering | Chat apps, task management |
21
+ | **Support** | Community (open source) | Official ClickUp |
22
+
23
+ **Choose this MCP when:**
24
+ - You need rich task context with inline images for AI coding tools
25
+ - You need API key authentication for automation or CI/CD pipelines
26
+ - You want the `read-minimal` mode optimized for development workflows
27
+
28
+ **Choose Official MCP when:**
29
+ - You need OAuth authentication for enterprise security compliance
30
+ - You need Chat integration or Connected Search features
31
+ - You want official support and no local installation
32
+
5
33
  ## What You Can Do
6
34
 
7
35
  Turn natural language into powerful ClickUp actions:
@@ -155,21 +183,22 @@ The ClickUp MCP supports three operational modes to balance functionality, secur
155
183
  - **📖 `read`**: Full read-only access for project exploration and workflow understanding
156
184
  - **✏️ `write`** (Default): Complete functionality for task management and productivity workflows
157
185
 
158
- | Tool | read-minimal | read | write | Description |
159
- |-------------------|:------------:|:----:|:-----:|-----------------------------------------------------------------------------------------|
160
- | `getTaskById` | ✅ | ✅ | ✅ | Get complete task details including comments, images, and metadata |
161
- | `addComment` | ❌ | ❌ | ✅ | Add comments to tasks for collaboration |
162
- | `updateTask` | ❌ | ❌ | ✅ | Update tasks (status, priority, assignees, etc.) with **SAFE APPEND-ONLY** descriptions |
163
- | `createTask` | ❌ | ❌ | ✅ | Create new tasks with full markdown support |
164
- | `searchTasks` | ✅ | ✅ | ✅ | Find tasks by content, keywords, assignees, or project context |
165
- | `searchSpaces` | ❌ | ✅ | ✅ | Browse workspace structure, project organization, and documents |
166
- | `getListInfo` | ❌ | ✅ | ✅ | Get list details and available statuses for task creation |
167
- | `updateListInfo` | ❌ | ❌ | ✅ | **SAFE APPEND-ONLY** updates to list descriptions (preserves existing content) |
168
- | `getTimeEntries` | ❌ | ✅ | ✅ | View time entries and analyze time spent across projects |
169
- | `createTimeEntry` | ❌ | ❌ | ✅ | Log time entries for task tracking |
170
- | `readDocument` | ❌ | ✅ | ✅ | Get document details, page structure, and content with navigation |
171
- | `searchDocuments` | ❌ | ✅ | ✅ | Search documents by name and space with fuzzy matching and space filtering |
172
- | `writeDocument` | ❌ | ❌ | ✅ | Universal document and page operations with smart document creation |
186
+ | Tool | read-minimal | read | write | Description |
187
+ |------------------------|:------------:|:----:|:-----:|-----------------------------------------------------------------------------------------|
188
+ | `getTaskById` | ✅ | ✅ | ✅ | Get complete task details including comments, images, and metadata |
189
+ | `addComment` | ❌ | ❌ | ✅ | Add comments to tasks for collaboration |
190
+ | `updateTask` | ❌ | ❌ | ✅ | Update tasks (status, priority, assignees, etc.) with **SAFE APPEND-ONLY** descriptions |
191
+ | `createTask` | ❌ | ❌ | ✅ | Create new tasks with full markdown support |
192
+ | `searchTasks` | ✅ | ✅ | ✅ | Find tasks by content, keywords, assignees, or project context |
193
+ | `searchSpaces` | ❌ | ✅ | ✅ | Browse workspace structure, project organization, and documents |
194
+ | `getListInfo` | ❌ | ✅ | ✅ | Get list details and available statuses for task creation |
195
+ | `updateListInfo` | ❌ | ❌ | ✅ | **SAFE APPEND-ONLY** updates to list descriptions (preserves existing content) |
196
+ | `getTimeEntries` | ❌ | ✅ | ✅ | View time entries and analyze time spent across projects |
197
+ | `createTimeEntry` | ❌ | ❌ | ✅ | Log time entries for task tracking |
198
+ | `readDocument` | ❌ | ✅ | ✅ | Get document details, page structure, and content with navigation |
199
+ | `searchDocuments` | ❌ | ✅ | ✅ | Search documents by name and space with fuzzy matching and space filtering |
200
+ | `updateDocumentPage` | ❌ | ❌ | ✅ | Update existing page content or name with replace/append modes |
201
+ | `createDocumentOrPage` | ❌ | ❌ | ✅ | Create new documents with first page, or add pages/sub-pages to existing documents |
173
202
 
174
203
  ### Setting the Mode
175
204
 
@@ -37,12 +37,47 @@ export interface ClickUpAttachment {
37
37
  * @param textItems Array of text items from ClickUp API
38
38
  * @returns Promise resolving to an array of content blocks (text and images)
39
39
  */
40
- export declare function processClickUpText(textItems: ClickUpTextItem[]): Promise<(CallToolResult["content"][number] | ImageMetadataBlock)[]>;
40
+ export declare function convertClickUpTextItemsToToolCallResult(textItems: ClickUpTextItem[]): Promise<(CallToolResult["content"][number] | ImageMetadataBlock)[]>;
41
41
  /**
42
42
  * Splits markdown text at image references and converts them to image blocks
43
43
  * @param markdownText The markdown text to process
44
44
  * @param attachments Array of attachments from the Clickup API
45
45
  * @returns Array of content blocks (text and images)
46
46
  */
47
- export declare function processClickUpMarkdown(markdownText: string, attachments: ClickUpAttachment[] | null | undefined): (CallToolResult["content"][number] | ImageMetadataBlock)[];
47
+ export declare function convertMarkdownToToolCallResult(markdownText: string, attachments: ClickUpAttachment[] | null | undefined): (CallToolResult["content"][number] | ImageMetadataBlock)[];
48
+ /**
49
+ * Represents a ClickUp comment block with formatting
50
+ */
51
+ export interface ClickUpCommentBlock {
52
+ text?: string;
53
+ type?: string;
54
+ attributes?: {
55
+ bold?: boolean;
56
+ italic?: boolean;
57
+ code?: boolean;
58
+ link?: string;
59
+ 'code-block'?: {
60
+ 'code-block': string;
61
+ };
62
+ header?: number;
63
+ blockquote?: {};
64
+ 'blockquote-size'?: 'large';
65
+ list?: {
66
+ list: 'bullet' | 'ordered' | 'unchecked' | 'checked';
67
+ };
68
+ indent?: number;
69
+ 'block-id'?: string;
70
+ };
71
+ list?: {
72
+ list: 'bullet' | 'ordered' | 'unchecked' | 'checked';
73
+ };
74
+ }
75
+ /**
76
+ * Convert markdown text to ClickUp comment blocks format using remark
77
+ * Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks
78
+ *
79
+ * @param markdown The markdown text to convert
80
+ * @returns Array of ClickUp comment blocks
81
+ */
82
+ export declare function convertMarkdownToClickUpBlocks(markdown: string): ClickUpCommentBlock[];
48
83
  //# 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;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGpD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AA4BD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAAE,GAC3B,OAAO,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAkGrE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAClD,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CA0I5D"}
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;AAOpD;;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,uCAAuC,CAC3D,SAAS,EAAE,eAAe,EAAE,GAC3B,OAAO,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAsNrE;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAClD,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAAC,EAAE,CA0I5D;AA8BD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,YAAY,CAAC,EAAE;YACb,YAAY,EAAE,MAAM,CAAC;SACtB,CAAC;QACF,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,EAAE,CAAC;QAChB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,IAAI,CAAC,EAAE;YACL,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;SACtD,CAAC;QACF,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;KACtD,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAoBtF"}
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processClickUpText = processClickUpText;
4
- exports.processClickUpMarkdown = processClickUpMarkdown;
6
+ exports.convertClickUpTextItemsToToolCallResult = convertClickUpTextItemsToToolCallResult;
7
+ exports.convertMarkdownToToolCallResult = convertMarkdownToToolCallResult;
8
+ exports.convertMarkdownToClickUpBlocks = convertMarkdownToClickUpBlocks;
5
9
  const data_uri_1 = require("./shared/data-uri");
10
+ const unified_1 = require("unified");
11
+ const remark_parse_1 = __importDefault(require("remark-parse"));
12
+ const remark_gfm_1 = __importDefault(require("remark-gfm"));
6
13
  /**
7
14
  * Extract thumbnail URLs from data-attachment attribute JSON
8
15
  * ClickUp API sometimes has broken thumbnail URLs, but data-attachment contains working ones
@@ -31,9 +38,14 @@ function extractThumbnailsFromDataAttachment(attributes) {
31
38
  * @param textItems Array of text items from ClickUp API
32
39
  * @returns Promise resolving to an array of content blocks (text and images)
33
40
  */
34
- async function processClickUpText(textItems) {
41
+ async function convertClickUpTextItemsToToolCallResult(textItems) {
35
42
  const contentBlocks = [];
36
43
  let currentTextBlock = "";
44
+ let currentLine = ""; // Track current line separately for block formatting
45
+ // Track current formatting state to avoid unnecessary close/reopen
46
+ let activeBold = false;
47
+ let activeItalic = false;
48
+ let activeCode = false;
37
49
  for (let i = 0; i < textItems.length; i++) {
38
50
  const item = textItems[i];
39
51
  // Handle image items
@@ -100,7 +112,108 @@ async function processClickUpText(textItems) {
100
112
  }
101
113
  // Handle text items
102
114
  else if (typeof item.text === "string") {
103
- currentTextBlock += item.text;
115
+ // Check if this is a newline with block formatting (header, blockquote, list)
116
+ if (item.text === '\n' && item.attributes) {
117
+ // Header formatting
118
+ if (item.attributes.header) {
119
+ const level = item.attributes.header;
120
+ currentLine = '#'.repeat(level) + ' ' + currentLine;
121
+ }
122
+ // Blockquote formatting
123
+ else if (item.attributes.blockquote) {
124
+ currentLine = '> ' + currentLine;
125
+ }
126
+ // List formatting
127
+ else if (item.attributes.list) {
128
+ const listType = item.attributes.list.list;
129
+ const indent = item.attributes.indent || 0;
130
+ // Add indentation (2 spaces per level) for nested lists
131
+ const indentStr = ' '.repeat(indent);
132
+ switch (listType) {
133
+ case 'bullet':
134
+ currentLine = indentStr + '- ' + currentLine;
135
+ break;
136
+ case 'ordered':
137
+ currentLine = indentStr + '1. ' + currentLine;
138
+ break;
139
+ case 'checked':
140
+ currentLine = indentStr + '- [x] ' + currentLine;
141
+ break;
142
+ case 'unchecked':
143
+ currentLine = indentStr + '- [ ] ' + currentLine;
144
+ break;
145
+ }
146
+ }
147
+ // Code block formatting
148
+ else if (item.attributes['code-block']) {
149
+ // Wrap the current line in code block markers
150
+ currentLine = '```\n' + currentLine + '\n```';
151
+ }
152
+ // Add formatted line to text block
153
+ currentTextBlock += currentLine;
154
+ // Add newline unless it's code block (already has newlines)
155
+ if (!item.attributes['code-block']) {
156
+ currentTextBlock += '\n';
157
+ }
158
+ currentLine = ""; // Reset for next line
159
+ continue;
160
+ }
161
+ // Regular text with inline formatting
162
+ let formattedText = item.text;
163
+ // Determine current and next formatting state
164
+ const hasBold = item.attributes?.bold === true;
165
+ const hasItalic = item.attributes?.italic === true;
166
+ const hasLink = item.attributes?.link;
167
+ // Look ahead to next non-newline block
168
+ let nextHasBold = false;
169
+ let nextHasItalic = false;
170
+ for (let j = i + 1; j < textItems.length; j++) {
171
+ const nextItem = textItems[j];
172
+ if (nextItem.text !== '\n' || !nextItem.attributes) {
173
+ nextHasBold = nextItem.attributes?.bold === true;
174
+ nextHasItalic = nextItem.attributes?.italic === true;
175
+ break;
176
+ }
177
+ }
178
+ // Build prefix (open new formatting)
179
+ let prefix = "";
180
+ if (hasBold && !activeBold)
181
+ prefix += "**";
182
+ if (hasItalic && !activeItalic)
183
+ prefix += "*";
184
+ // Build suffix (close formatting that won't continue)
185
+ let suffix = "";
186
+ if (hasItalic && !nextHasItalic)
187
+ suffix += "*";
188
+ if (hasBold && !nextHasBold)
189
+ suffix += "**";
190
+ // Close formatting that's active but not in this block
191
+ let closingPrefix = "";
192
+ if (activeBold && !hasBold)
193
+ closingPrefix += "**";
194
+ if (activeItalic && !hasItalic)
195
+ closingPrefix += "*";
196
+ formattedText = closingPrefix + prefix + formattedText + suffix;
197
+ // Update state
198
+ activeBold = hasBold && nextHasBold;
199
+ activeItalic = hasItalic && nextHasItalic;
200
+ // Link formatting (wraps everything)
201
+ if (hasLink) {
202
+ formattedText = `[${formattedText}](${hasLink})`;
203
+ }
204
+ // Code formatting
205
+ if (item.attributes?.code) {
206
+ formattedText = `\`${formattedText}\``;
207
+ }
208
+ // Add to current line (not text block yet)
209
+ if (item.text === '\n') {
210
+ // Plain newline without formatting
211
+ currentTextBlock += currentLine + '\n';
212
+ currentLine = "";
213
+ }
214
+ else {
215
+ currentLine += formattedText;
216
+ }
104
217
  }
105
218
  // Handle other types of items like bookmarks or whatever clickup can think of
106
219
  else {
@@ -108,6 +221,9 @@ async function processClickUpText(textItems) {
108
221
  }
109
222
  }
110
223
  // Add any remaining text
224
+ if (currentLine) {
225
+ currentTextBlock += currentLine;
226
+ }
111
227
  if (currentTextBlock.trim()) {
112
228
  contentBlocks.push({
113
229
  type: "text",
@@ -122,7 +238,7 @@ async function processClickUpText(textItems) {
122
238
  * @param attachments Array of attachments from the Clickup API
123
239
  * @returns Array of content blocks (text and images)
124
240
  */
125
- function processClickUpMarkdown(markdownText, attachments) {
241
+ function convertMarkdownToToolCallResult(markdownText, attachments) {
126
242
  const contentBlocks = [];
127
243
  let currentTextBlock = "";
128
244
  // Create a map of attachment URLs to their full info for easy lookup
@@ -264,3 +380,184 @@ function extractFileTypeFromUrl(url) {
264
380
  return null;
265
381
  return filename.substring(lastDot + 1);
266
382
  }
383
+ /**
384
+ * Convert markdown text to ClickUp comment blocks format using remark
385
+ * Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks
386
+ *
387
+ * @param markdown The markdown text to convert
388
+ * @returns Array of ClickUp comment blocks
389
+ */
390
+ function convertMarkdownToClickUpBlocks(markdown) {
391
+ const blocks = [];
392
+ try {
393
+ // Parse the entire markdown document using remark with GFM support (for task lists)
394
+ const tree = (0, unified_1.unified)()
395
+ .use(remark_parse_1.default)
396
+ .use(remark_gfm_1.default)
397
+ .parse(markdown);
398
+ // Walk the tree recursively
399
+ walkMdastNodes(tree.children, {}, blocks);
400
+ }
401
+ catch (error) {
402
+ console.error('Failed to parse markdown:', error);
403
+ // Fallback to plain text
404
+ return [{ text: markdown, attributes: {} }];
405
+ }
406
+ return blocks;
407
+ }
408
+ /**
409
+ * Recursively walk mdast nodes and convert to ClickUp blocks
410
+ * @param nodes Array of mdast nodes to process
411
+ * @param inheritedAttrs Formatting attributes inherited from parent nodes
412
+ * @param blocks Output array to append ClickUp blocks to
413
+ * @param depth Nesting depth for lists (0 = top level, 1 = first nest, etc.)
414
+ */
415
+ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
416
+ for (let i = 0; i < nodes.length; i++) {
417
+ const node = nodes[i];
418
+ const currentAttrs = { ...inheritedAttrs };
419
+ switch (node.type) {
420
+ case 'heading':
421
+ // Process heading content with inline formatting
422
+ walkPhrasingContent(node.children, currentAttrs, blocks);
423
+ // Add newline with header attribute
424
+ blocks.push({ text: '\n', attributes: { header: node.depth } });
425
+ break;
426
+ case 'paragraph':
427
+ // Process paragraph content with inline formatting
428
+ walkPhrasingContent(node.children, currentAttrs, blocks);
429
+ // Add newline unless it's the last node
430
+ if (i < nodes.length - 1) {
431
+ blocks.push({ text: '\n', attributes: {} });
432
+ }
433
+ break;
434
+ case 'blockquote':
435
+ // ClickUp limitation: Blockquotes only support paragraph content, not headers or lists
436
+ // For complex blockquote content (headers, lists), only paragraph text is preserved
437
+ const blockquoteChildren = node.children;
438
+ for (const child of blockquoteChildren) {
439
+ if (child.type === 'paragraph') {
440
+ walkPhrasingContent(child.children, currentAttrs, blocks);
441
+ blocks.push({ text: '\n', attributes: { blockquote: {} } });
442
+ }
443
+ // Note: Other child types (heading, list) are not supported by ClickUp blockquotes
444
+ // and will be silently skipped, preserving only inline paragraph content
445
+ }
446
+ break;
447
+ case 'list':
448
+ const listNode = node;
449
+ const listType = listNode.ordered ? 'ordered' : 'bullet';
450
+ for (const item of listNode.children) {
451
+ const listItem = item;
452
+ // Check if it's a checkbox item
453
+ const isChecked = listItem.checked === true;
454
+ const isUnchecked = listItem.checked === false;
455
+ const finalListType = isChecked ? 'checked' : isUnchecked ? 'unchecked' : listType;
456
+ // Process list item content
457
+ for (const itemChild of listItem.children) {
458
+ if (itemChild.type === 'paragraph') {
459
+ // Process paragraph content with inline formatting
460
+ walkPhrasingContent(itemChild.children, currentAttrs, blocks);
461
+ // Add newline with list formatting and optional indent
462
+ const listAttrs = {
463
+ list: { list: finalListType }
464
+ };
465
+ // Add indent for nested lists (depth 0 = no indent, depth 1+ = indented)
466
+ if (depth > 0) {
467
+ listAttrs.indent = depth;
468
+ }
469
+ blocks.push({ text: '\n', attributes: listAttrs });
470
+ }
471
+ else if (itemChild.type === 'list') {
472
+ // Nested list - recursively process with increased depth
473
+ walkMdastNodes([itemChild], currentAttrs, blocks, depth + 1);
474
+ }
475
+ }
476
+ }
477
+ break;
478
+ case 'code':
479
+ // Code block
480
+ const codeNode = node;
481
+ if (codeNode.value) {
482
+ blocks.push({ text: codeNode.value, attributes: {} });
483
+ blocks.push({
484
+ text: '\n',
485
+ attributes: { 'code-block': { 'code-block': codeNode.lang || 'plain' } }
486
+ });
487
+ }
488
+ break;
489
+ case 'thematicBreak':
490
+ // Horizontal rule - just add a line break
491
+ blocks.push({ text: '\n', attributes: {} });
492
+ break;
493
+ default:
494
+ // For any other block-level nodes, try to process children
495
+ if ('children' in node && Array.isArray(node.children)) {
496
+ walkMdastNodes(node.children, currentAttrs, blocks, depth);
497
+ }
498
+ break;
499
+ }
500
+ }
501
+ }
502
+ /**
503
+ * Recursively walk phrasing content (inline nodes) and build ClickUp blocks
504
+ * Accumulates formatting attributes from parent nodes
505
+ */
506
+ function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
507
+ for (const node of nodes) {
508
+ const currentAttrs = { ...inheritedAttrs };
509
+ switch (node.type) {
510
+ case 'text':
511
+ // Plain text node
512
+ if (node.value) {
513
+ blocks.push({
514
+ text: node.value,
515
+ attributes: Object.keys(currentAttrs).length > 0 ? currentAttrs : {}
516
+ });
517
+ }
518
+ break;
519
+ case 'strong':
520
+ // Bold text - recurse with bold attribute
521
+ currentAttrs.bold = true;
522
+ walkPhrasingContent(node.children, currentAttrs, blocks);
523
+ break;
524
+ case 'emphasis':
525
+ // Italic text - recurse with italic attribute
526
+ currentAttrs.italic = true;
527
+ walkPhrasingContent(node.children, currentAttrs, blocks);
528
+ break;
529
+ case 'inlineCode':
530
+ // Inline code
531
+ if (node.value) {
532
+ currentAttrs.code = true;
533
+ blocks.push({
534
+ text: node.value,
535
+ attributes: currentAttrs
536
+ });
537
+ }
538
+ break;
539
+ case 'link':
540
+ // Link - recurse with link attribute
541
+ currentAttrs.link = node.url;
542
+ walkPhrasingContent(node.children, currentAttrs, blocks);
543
+ break;
544
+ case 'break':
545
+ // Line break - add as plain text
546
+ blocks.push({ text: '\n', attributes: {} });
547
+ break;
548
+ default:
549
+ // For any other node types, try to extract text if available
550
+ if ('value' in node && typeof node.value === 'string') {
551
+ blocks.push({
552
+ text: node.value,
553
+ attributes: Object.keys(currentAttrs).length > 0 ? currentAttrs : {}
554
+ });
555
+ }
556
+ else if ('children' in node && Array.isArray(node.children)) {
557
+ // Recurse into children for other container nodes
558
+ walkPhrasingContent(node.children, currentAttrs, blocks);
559
+ }
560
+ break;
561
+ }
562
+ }
563
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"image-processing.d.ts","sourceRoot":"","sources":["../../src/shared/image-processing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAKzD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,EAAE,SAAS,GAAE,MAAyB,EAAE,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BhM"}
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;AAmDzD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,EAAE,SAAS,GAAE,MAAyB,EAAE,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BhM"}
@@ -4,6 +4,46 @@ exports.downloadImages = downloadImages;
4
4
  const config_1 = require("./config");
5
5
  const data_uri_1 = require("./data-uri");
6
6
  const buffer_1 = require("buffer");
7
+ /**
8
+ * Detect MIME type from image binary data using magic bytes (file signatures)
9
+ * Returns null if the format is not recognized
10
+ */
11
+ function detectMimeTypeFromBuffer(buffer) {
12
+ const bytes = new Uint8Array(buffer);
13
+ if (bytes.length < 12)
14
+ return null;
15
+ // PNG: 89 50 4E 47 (‰PNG)
16
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
17
+ return "image/png";
18
+ }
19
+ // JPEG: FF D8 FF
20
+ if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
21
+ return "image/jpeg";
22
+ }
23
+ // GIF: 47 49 46 38 (GIF8)
24
+ if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) {
25
+ return "image/gif";
26
+ }
27
+ // WebP: RIFF....WEBP
28
+ if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
29
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
30
+ return "image/webp";
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Detect MIME type from base64-encoded image data
36
+ * Decodes just enough bytes to check magic numbers
37
+ */
38
+ function detectMimeTypeFromBase64(base64Data) {
39
+ // Need at least 16 base64 chars to decode 12 bytes for magic number detection
40
+ if (base64Data.length < 16)
41
+ return null;
42
+ // Create a Uint8Array directly from the decoded bytes to avoid Buffer pooling issues
43
+ const header = buffer_1.Buffer.from(base64Data.slice(0, 16), "base64");
44
+ const bytes = new Uint8Array(header);
45
+ return detectMimeTypeFromBuffer(bytes.buffer);
46
+ }
7
47
  /**
8
48
  * Downloads images from image_metadata blocks and applies smart size/count limiting
9
49
  * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
@@ -98,9 +138,12 @@ async function downloadSingleImage(imageMetadata, perImageBudget) {
98
138
  const actualSizeBytes = imageBuffer.byteLength;
99
139
  // Double-check actual size (in case Content-Length was missing or incorrect)
100
140
  if (actualSizeBytes <= perImageBudget) {
141
+ // Detect actual MIME type from binary data, fall back to header or default
142
+ const detectedMimeType = detectMimeTypeFromBuffer(imageBuffer);
143
+ const mimeType = detectedMimeType || response.headers.get("Content-Type") || "image/png";
101
144
  return {
102
145
  type: "image",
103
- mimeType: response.headers.get("Content-Type") || "image/png",
146
+ mimeType,
104
147
  data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
105
148
  };
106
149
  }
@@ -145,9 +188,12 @@ function convertInlineImage(imageMetadata, perImageBudget) {
145
188
  console.error(`Inline image for "${imageMetadata.alt}" is ${estimatedSize} bytes, exceeds budget of ${perImageBudget} bytes`);
146
189
  return createImageFallback(imageMetadata);
147
190
  }
191
+ // Detect actual MIME type from binary data, fall back to declared type or default
192
+ const detectedMimeType = detectMimeTypeFromBase64(inlineData.base64Data);
193
+ const mimeType = detectedMimeType || inlineData.mimeType || "image/png";
148
194
  return {
149
195
  type: "image",
150
- mimeType: inlineData.mimeType || "image/png",
196
+ mimeType,
151
197
  data: inlineData.base64Data,
152
198
  };
153
199
  }
@@ -93,7 +93,7 @@ async function loadTaskContent(taskId) {
93
93
  return await generateTaskMetadata(task, timeEntries, true);
94
94
  })(),
95
95
  // process markdown and download images
96
- (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments || []),
96
+ (0, clickup_text_1.convertMarkdownToToolCallResult)(task.markdown_description || "", task.attachments || []),
97
97
  ]);
98
98
  return [taskMetadata, ...content];
99
99
  }
@@ -114,7 +114,7 @@ async function loadTaskComments(id) {
114
114
  type: "text",
115
115
  text: `Comment by ${comment.user.username} on ${timestampToIso(comment.date)}:`,
116
116
  };
117
- const commentBodyBlocks = await (0, clickup_text_1.processClickUpText)(comment.comment);
117
+ const commentBodyBlocks = await (0, clickup_text_1.convertClickUpTextItemsToToolCallResult)(comment.comment);
118
118
  return {
119
119
  date: comment.date, // String timestamp from ClickUp for sorting
120
120
  contentBlocks: [headerBlock, ...commentBodyBlocks],
@@ -1 +1 @@
1
- {"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAapE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAmZtE"}
1
+ {"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAsZtE"}
@@ -4,6 +4,7 @@ exports.registerTaskToolsWrite = registerTaskToolsWrite;
4
4
  const zod_1 = require("zod");
5
5
  const config_1 = require("../shared/config");
6
6
  const utils_1 = require("../shared/utils");
7
+ const clickup_text_1 = require("../clickup-text");
7
8
  // Shared schemas for task parameters
8
9
  const taskNameSchema = zod_1.z.string().min(1).describe("The name/title of the task");
9
10
  const taskPrioritySchema = zod_1.z.enum(["urgent", "high", "normal", "low"]).optional().describe("Optional priority level");
@@ -36,8 +37,10 @@ function registerTaskToolsWrite(server, userData) {
36
37
  idempotentHint: false,
37
38
  }, async ({ task_id, comment }) => {
38
39
  try {
40
+ // Convert markdown to ClickUp formatted blocks
41
+ const commentBlocks = (0, clickup_text_1.convertMarkdownToClickUpBlocks)(comment);
39
42
  const requestBody = {
40
- comment_text: comment,
43
+ comment: commentBlocks,
41
44
  notify_all: true
42
45
  };
43
46
  const response = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/comment`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Search, create, and retrieve tasks, add comments, and track time through natural language commands.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,12 +40,16 @@
40
40
  },
41
41
  "license": "MIT",
42
42
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.15.1",
43
+ "@modelcontextprotocol/sdk": "1.15.1",
44
44
  "fuse.js": "^7.1.0",
45
+ "remark-gfm": "^4.0.1",
46
+ "remark-parse": "^11.0.0",
47
+ "unified": "^11.0.5",
45
48
  "zod": "^3.24.2"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@anthropic-ai/mcpb": "^1.1.1",
52
+ "@types/mdast": "^4.0.4",
49
53
  "@types/node": "^22.14.1",
50
54
  "dotenv": "^16.5.0",
51
55
  "nodemon": "^3.1.9",