@hauptsache.net/clickup-mcp
Version:
Transform your AI assistant into a powerful ClickUp integration for both agentic coding and productivity management. Enables seamless task context sharing, intelligent search, time tracking, and complete project management workflows.
204 lines (203 loc) • 8.94 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.processClickUpText = processClickUpText;
exports.processClickUpMarkdown = processClickUpMarkdown;
/**
* Extract thumbnail URLs from data-attachment attribute JSON
* ClickUp API sometimes has broken thumbnail URLs, but data-attachment contains working ones
*/
function extractThumbnailsFromDataAttachment(attributes) {
if (!attributes || !attributes['data-attachment']) {
return {};
}
try {
const attachmentData = JSON.parse(attributes['data-attachment']);
return {
thumbnail_large: attachmentData.thumbnail_large,
thumbnail_medium: attachmentData.thumbnail_medium,
thumbnail_small: attachmentData.thumbnail_small,
};
}
catch (error) {
console.error('Error parsing data-attachment:', error);
return {};
}
}
/**
* Process an array of ClickUp text items into a structured content format
* that includes both text and images in their original sequence
*
* @param textItems Array of text items from ClickUp API
* @returns Promise resolving to an array of content blocks (text and images)
*/
async function processClickUpText(textItems) {
const contentBlocks = [];
let currentTextBlock = "";
for (let i = 0; i < textItems.length; i++) {
const item = textItems[i];
// Handle image items
if (item.type === "image" && item.image && item.image.url) {
// Add image URL reference inline to current text block
const imageFileName = item.image.name || item.image.title || "image";
currentTextBlock += `\nImage: ${imageFileName} - ${item.image.url}`;
// Get working thumbnail URLs from data-attachment if available
const extractedThumbnails = extractThumbnailsFromDataAttachment(item.attributes);
// Determine best thumbnail URLs (prefer extracted over API thumbnails)
const thumbnail_large = extractedThumbnails.thumbnail_large || item.image.thumbnail_large;
const thumbnail_medium = extractedThumbnails.thumbnail_medium || item.image.thumbnail_medium;
const thumbnail_small = extractedThumbnails.thumbnail_small || item.image.thumbnail_small;
// Only create image_metadata if we have at least one thumbnail (never use original image)
if (thumbnail_large || thumbnail_medium || thumbnail_small) {
// Push accumulated text (including image URL) as a text block
if (currentTextBlock.trim()) {
contentBlocks.push({
type: "text",
text: currentTextBlock.trim(),
});
}
// Reset current text block after pushing it
currentTextBlock = "";
// Create URLs array with largest to smallest preference, filter out undefined
const urls = [thumbnail_large, thumbnail_medium, thumbnail_small].filter(Boolean);
// Add image_metadata block for lazy loading
contentBlocks.push({
type: "image_metadata",
urls: urls,
alt: item.text || imageFileName,
});
}
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
}
// Handle text items
else if (typeof item.text === "string") {
currentTextBlock += item.text;
}
// Handle other types of items like bookmarks or whatever clickup can think of
else {
currentTextBlock += JSON.stringify(item);
}
}
// Add any remaining text
if (currentTextBlock.trim()) {
contentBlocks.push({
type: "text",
text: currentTextBlock.trim(),
});
}
return contentBlocks;
}
/**
* Splits markdown text at image references and converts them to image blocks
* @param markdownText The markdown text to process
* @param attachments Array of attachments from the Clickup API
* @returns Array of content blocks (text and images)
*/
function processClickUpMarkdown(markdownText, attachments) {
const contentBlocks = [];
let currentTextBlock = "";
// Create a map of attachment URLs to their full info for easy lookup
const attachmentMap = new Map();
for (const attachment of attachments) {
attachmentMap.set(attachment.url, attachment);
}
// Regular expression to match markdown image syntax: 
const imageRegex = /!\[([^\]]*)\]\(([^\)]+)\)/g;
let lastIndex = 0;
let match;
while ((match = imageRegex.exec(markdownText)) !== null) {
const [fullMatch, altText, imageUrl] = match;
// Add text before the image reference to the current text block
currentTextBlock += markdownText.substring(lastIndex, match.index);
// Check if this image URL exists in our attachments
const attachment = attachmentMap.get(imageUrl);
if (attachment) {
// Add image URL reference inline to current text block
const imageFileName = altText || "image";
currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
// Only create image_metadata if we have at least one thumbnail (never use original image)
if (attachment.thumbnail_large || attachment.thumbnail_medium || attachment.thumbnail_small) {
// Push accumulated text (including image URL) as a text block
if (currentTextBlock.trim()) {
contentBlocks.push({
type: "text",
text: currentTextBlock.trim(),
});
}
// Reset current text block after pushing it
currentTextBlock = "";
// Create URLs array with largest to smallest preference, filter out undefined
const urls = [attachment.thumbnail_large, attachment.thumbnail_medium, attachment.thumbnail_small].filter(Boolean);
// Add image_metadata block for lazy loading
contentBlocks.push({
type: "image_metadata",
urls: urls,
alt: altText || imageFileName,
});
}
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
}
else {
// If the image URL doesn't match any attachment, keep the original markdown in the current text block
currentTextBlock += fullMatch;
console.error(`Image URL ${imageUrl} not found in attachments`, attachmentMap);
}
lastIndex = match.index + fullMatch.length;
}
// Add any remaining text after the last image
currentTextBlock += markdownText.substring(lastIndex);
// Process non-image attachments that weren't referenced in markdown
const referencedUrls = new Set();
const imageMatches = markdownText.matchAll(/!\[([^\]]*)\]\(([^\)]+)\)/g);
for (const match of imageMatches) {
referencedUrls.add(match[2]);
}
// Add non-image files inline to the current text block
for (const attachment of attachments) {
if (!referencedUrls.has(attachment.url)) {
// Determine if this is an image based on URL or type
const isImage = attachment.thumbnail_large ||
/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(attachment.url);
if (!isImage) {
// This is a non-image file - add inline to current text block
const fileName = extractFileNameFromUrl(attachment.url) || "file";
const fileType = extractFileTypeFromUrl(attachment.url);
const fileTypeText = fileType ? ` (${fileType.toUpperCase()})` : "";
currentTextBlock += `\nFile: ${fileName}${fileTypeText} - ${attachment.url}`;
}
}
}
// Add any remaining text (including file references) as final text block
if (currentTextBlock.trim()) {
contentBlocks.push({
type: "text",
text: currentTextBlock.trim(),
});
}
return contentBlocks;
}
/**
* Extract filename from URL
*/
function extractFileNameFromUrl(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const filename = pathname.split('/').pop();
return filename && filename !== '' ? filename : null;
}
catch {
return null;
}
}
/**
* Extract file extension from URL
*/
function extractFileTypeFromUrl(url) {
const filename = extractFileNameFromUrl(url);
if (!filename)
return null;
const lastDot = filename.lastIndexOf('.');
if (lastDot === -1)
return null;
return filename.substring(lastDot + 1);
}