@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 +44 -15
- package/dist/clickup-text.d.ts +37 -2
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +302 -5
- package/dist/shared/image-processing.d.ts.map +1 -1
- package/dist/shared/image-processing.js +48 -2
- package/dist/tools/task-tools.js +2 -2
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +4 -1
- package/package.json +6 -2
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
|
|
159
|
-
|
|
160
|
-
| `getTaskById`
|
|
161
|
-
| `addComment`
|
|
162
|
-
| `updateTask`
|
|
163
|
-
| `createTask`
|
|
164
|
-
| `searchTasks`
|
|
165
|
-
| `searchSpaces`
|
|
166
|
-
| `getListInfo`
|
|
167
|
-
| `updateListInfo`
|
|
168
|
-
| `getTimeEntries`
|
|
169
|
-
| `createTimeEntry`
|
|
170
|
-
| `readDocument`
|
|
171
|
-
| `searchDocuments`
|
|
172
|
-
| `
|
|
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
|
|
package/dist/clickup-text.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/clickup-text.js
CHANGED
|
@@ -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.
|
|
4
|
-
exports.
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
196
|
+
mimeType,
|
|
151
197
|
data: inlineData.base64Data,
|
|
152
198
|
};
|
|
153
199
|
}
|
package/dist/tools/task-tools.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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;
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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",
|