@hauptsache.net/clickup-mcp 1.5.0 → 1.6.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
@@ -155,21 +155,22 @@ The ClickUp MCP supports three operational modes to balance functionality, secur
155
155
  - **📖 `read`**: Full read-only access for project exploration and workflow understanding
156
156
  - **✏️ `write`** (Default): Complete functionality for task management and productivity workflows
157
157
 
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 |
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
+ | `updateDocumentPage` | ❌ | ❌ | ✅ | Update existing page content or name with replace/append modes |
173
+ | `createDocumentOrPage` | ❌ | ❌ | ✅ | Create new documents with first page, or add pages/sub-pages to existing documents |
173
174
 
174
175
  ### Setting the Mode
175
176
 
@@ -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
+ }
@@ -34,8 +34,8 @@ function registerSpaceResources(server) {
34
34
  return {
35
35
  resources: activeSpaces.map((space) => ({
36
36
  uri: `clickup://space/${space.id}`,
37
- name: space.name,
38
- title: space.name,
37
+ name: `${space.name} ClickUp Space.txt`,
38
+ title: `${space.name} ClickUp Space`,
39
39
  mimeType: "text/plain"
40
40
  }))
41
41
  };
@@ -1 +1 @@
1
- {"version":3,"file":"doc-tools.d.ts","sourceRoot":"","sources":["../../src/tools/doc-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoDpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,QA0J1D;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,QA+R3D"}
1
+ {"version":3,"file":"doc-tools.d.ts","sourceRoot":"","sources":["../../src/tools/doc-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoDpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,QA0J1D;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,QAoS3D"}
@@ -209,7 +209,9 @@ function registerDocumentToolsWrite(server) {
209
209
  .optional()
210
210
  .describe("Whether to append content to existing page content (default: false - replaces content)")
211
211
  }, {
212
- readOnlyHint: false
212
+ readOnlyHint: false,
213
+ destructiveHint: true,
214
+ idempotentHint: false,
213
215
  }, async ({ doc_id, page_id, name, content, append = false }) => {
214
216
  try {
215
217
  const requestBody = {};
@@ -306,7 +308,10 @@ function registerDocumentToolsWrite(server) {
306
308
  .optional()
307
309
  .describe("Optional: page content in markdown format")
308
310
  }, {
309
- readOnlyHint: false
311
+ readOnlyHint: false,
312
+ destructiveHint: false,
313
+ idempotentHint: false,
314
+ openWorldHint: true
310
315
  }, async ({ space_id, list_id, doc_id, parent_page_id, name, content }) => {
311
316
  try {
312
317
  // Validate mutually exclusive parameters
@@ -1 +1 @@
1
- {"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAkHtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QA4EvD"}
1
+ {"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAkHtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QA8EvD"}
@@ -118,7 +118,9 @@ function registerListToolsWrite(server) {
118
118
  list_id: zod_1.z.string().min(1).describe("The list ID to update"),
119
119
  append_description: zod_1.z.string().min(1).describe("Markdown content to APPEND to existing list description (preserves existing content for safety)")
120
120
  }, {
121
- readOnlyHint: false
121
+ readOnlyHint: false,
122
+ destructiveHint: false,
123
+ idempotentHint: false,
122
124
  }, async ({ list_id, append_description }) => {
123
125
  try {
124
126
  // Get current list info including description (try to get markdown content)
@@ -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,QA2YtE"}
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");
@@ -31,11 +32,15 @@ function registerTaskToolsWrite(server, userData) {
31
32
  task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to comment on"),
32
33
  comment: zod_1.z.string().min(1).describe("The comment text to add to the task"),
33
34
  }, {
34
- readOnlyHint: false
35
+ readOnlyHint: false,
36
+ destructiveHint: false,
37
+ idempotentHint: false,
35
38
  }, async ({ task_id, comment }) => {
36
39
  try {
40
+ // Convert markdown to ClickUp formatted blocks
41
+ const commentBlocks = (0, clickup_text_1.convertMarkdownToClickUpBlocks)(comment);
37
42
  const requestBody = {
38
- comment_text: comment,
43
+ comment: commentBlocks,
39
44
  notify_all: true
40
45
  };
41
46
  const response = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/comment`, {
@@ -111,7 +116,10 @@ function registerTaskToolsWrite(server, userData) {
111
116
  blocking: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs that this task should block. Note: This creates dependencies FROM those tasks TO this task (those tasks will wait on this one)"),
112
117
  linked_tasks: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs to link as related tasks without blocking (will replace existing linked tasks)")
113
118
  }, {
114
- readOnlyHint: false
119
+ readOnlyHint: false,
120
+ destructiveHint: true,
121
+ idempotentHint: false,
122
+ openWorldHint: true
115
123
  }, async ({ task_id, name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees, blocking, waiting_on, linked_tasks }) => {
116
124
  try {
117
125
  const userData = await (0, utils_1.getCurrentUser)();
@@ -289,7 +297,10 @@ function registerTaskToolsWrite(server, userData) {
289
297
  parent_task_id: zod_1.z.string().optional().describe("Optional parent task ID to create this as a subtask"),
290
298
  assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
291
299
  }, {
292
- readOnlyHint: false
300
+ readOnlyHint: false,
301
+ destructiveHint: false,
302
+ idempotentHint: false,
303
+ openWorldHint: true
293
304
  }, async ({ list_id, name, description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees }) => {
294
305
  try {
295
306
  const userData = await (0, utils_1.getCurrentUser)();
@@ -1 +1 @@
1
- {"version":3,"file":"time-tools.d.ts","sourceRoot":"","sources":["../../src/tools/time-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAyDpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAgFtD;AAwLD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAsFvD"}
1
+ {"version":3,"file":"time-tools.d.ts","sourceRoot":"","sources":["../../src/tools/time-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAyDpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAgFtD;AAwLD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAwFvD"}
@@ -271,7 +271,9 @@ function registerTimeToolsWrite(server) {
271
271
  description: zod_1.z.string().optional().describe("Optional description for the time entry"),
272
272
  start_time: zod_1.z.string().optional().describe("Optional start time as ISO date string (e.g., '2024-10-06T09:00:00+02:00', defaults to current time)")
273
273
  }, {
274
- readOnlyHint: false
274
+ readOnlyHint: false,
275
+ destructiveHint: false,
276
+ idempotentHint: false,
275
277
  }, async ({ task_id, hours, description, start_time }) => {
276
278
  try {
277
279
  // Convert hours to milliseconds (ClickUp API uses milliseconds)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
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",
@@ -42,10 +42,14 @@
42
42
  "dependencies": {
43
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",