@pripanggalih/clickup-mcp 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/LICENSE +22 -0
- package/README.md +295 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/clickup-text.d.ts +83 -0
- package/dist/clickup-text.d.ts.map +1 -0
- package/dist/clickup-text.js +563 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/resources/space-resources.d.ts +6 -0
- package/dist/resources/space-resources.d.ts.map +1 -0
- package/dist/resources/space-resources.js +95 -0
- package/dist/shared/config.d.ts +11 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +61 -0
- package/dist/shared/data-uri.d.ts +14 -0
- package/dist/shared/data-uri.d.ts.map +1 -0
- package/dist/shared/data-uri.js +34 -0
- package/dist/shared/image-processing.d.ts +13 -0
- package/dist/shared/image-processing.d.ts.map +1 -0
- package/dist/shared/image-processing.js +199 -0
- package/dist/shared/types.d.ts +21 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +71 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +508 -0
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +44 -0
- package/dist/tools/admin-tools.d.ts +3 -0
- package/dist/tools/admin-tools.d.ts.map +1 -0
- package/dist/tools/admin-tools.js +288 -0
- package/dist/tools/doc-tools.d.ts +4 -0
- package/dist/tools/doc-tools.d.ts.map +1 -0
- package/dist/tools/doc-tools.js +436 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +175 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +161 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +128 -0
- package/dist/tools/task-tools.d.ts +8 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +329 -0
- package/dist/tools/task-write-tools.d.ts +3 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -0
- package/dist/tools/task-write-tools.js +567 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +338 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.serverPromise = void 0;
|
|
5
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
const config_1 = require("./shared/config");
|
|
8
|
+
const utils_1 = require("./shared/utils");
|
|
9
|
+
// Import tool registration functions
|
|
10
|
+
const task_tools_1 = require("./tools/task-tools");
|
|
11
|
+
const task_write_tools_1 = require("./tools/task-write-tools");
|
|
12
|
+
const search_tools_1 = require("./tools/search-tools");
|
|
13
|
+
const space_tools_1 = require("./tools/space-tools");
|
|
14
|
+
const list_tools_1 = require("./tools/list-tools");
|
|
15
|
+
const time_tools_1 = require("./tools/time-tools");
|
|
16
|
+
const doc_tools_1 = require("./tools/doc-tools");
|
|
17
|
+
const admin_tools_1 = require("./tools/admin-tools");
|
|
18
|
+
const space_resources_1 = require("./resources/space-resources");
|
|
19
|
+
// Create server variable that will be initialized later
|
|
20
|
+
let server;
|
|
21
|
+
// Register tools based on mode with user data for enhanced documentation
|
|
22
|
+
async function initializeServer() {
|
|
23
|
+
console.error(`Starting ClickUp MCP in ${config_1.CONFIG.mode} mode`);
|
|
24
|
+
// Fetch current user and spaces for enhanced tool documentation and API health check
|
|
25
|
+
const [userData, spacesIndex] = await Promise.all([
|
|
26
|
+
(0, utils_1.getCurrentUser)(),
|
|
27
|
+
(0, utils_1.getSpaceSearchIndex)()
|
|
28
|
+
]);
|
|
29
|
+
const spaces = spacesIndex._docs || [];
|
|
30
|
+
console.error(`Connected as: ${userData.user.username} (${userData.user.email})`);
|
|
31
|
+
// Filter out archived spaces and format as simple list
|
|
32
|
+
const activeSpaces = spaces.filter((s) => !s.archived);
|
|
33
|
+
const formattedSpaces = activeSpaces
|
|
34
|
+
.map((s) => `- ${s.name} (space_id: ${s.id})`)
|
|
35
|
+
.join('\n');
|
|
36
|
+
const instructions = [
|
|
37
|
+
`ClickUp is a Ticket system. It is used to track tasks, bugs, and other work items.`,
|
|
38
|
+
`Is you are asked for infos about projects or tasks, search for tasks or documents in ClickUp (this MCP) first.`,
|
|
39
|
+
`The following spaces/projects are available:`,
|
|
40
|
+
formattedSpaces
|
|
41
|
+
].join('\n');
|
|
42
|
+
console.error(`Pre-loaded ${activeSpaces.length} active spaces`);
|
|
43
|
+
// Create the MCP server with instructions
|
|
44
|
+
server = new mcp_js_1.McpServer({
|
|
45
|
+
name: "Clickup MCP",
|
|
46
|
+
version: require('../package.json').version,
|
|
47
|
+
}, {
|
|
48
|
+
instructions
|
|
49
|
+
});
|
|
50
|
+
// Register prompts
|
|
51
|
+
const lang = config_1.CONFIG.primaryLanguageHint === 'de' ? 'de' : 'en';
|
|
52
|
+
// Register "my-todos" prompt
|
|
53
|
+
server.registerPrompt("my-todos", {
|
|
54
|
+
title: lang === 'de' ? "Meine TODOs" : "My TODOs",
|
|
55
|
+
description: lang === 'de'
|
|
56
|
+
? "Meine aktuellen TODO-Aufgaben aus ClickUp abrufen und nach Priorität kategorisiert analysieren"
|
|
57
|
+
: "Get and analyze my current TODO tasks from ClickUp, categorized by priority"
|
|
58
|
+
}, () => {
|
|
59
|
+
const messages = [{
|
|
60
|
+
role: "user",
|
|
61
|
+
content: {
|
|
62
|
+
type: "text",
|
|
63
|
+
text: lang === 'de'
|
|
64
|
+
? `Kannst du in ClickUp nachsehen, was meine aktuellen TODOs sind? Bitte suche nach allen offenen Aufgaben, die mir zugewiesen sind, analysiere deren Inhalt und kategorisiere sie nach erkennbarer Priorität (dringend, hoch, normal, niedrig). Für jede Kategorie gib eine kurze Zusammenfassung dessen, was getan werden muss und hebe Fälligkeitstermine oder wichtige Details aus den Aufgabenbeschreibungen hervor.
|
|
65
|
+
|
|
66
|
+
Bitte strukturiere deine Antwort mit:
|
|
67
|
+
1. **Zusammenfassung**: Gesamtanzahl der Aufgaben und allgemeine Prioritätsverteilung
|
|
68
|
+
2. **Dringende Aufgaben**: Aufgaben, die sofortige Aufmerksamkeit benötigen (heute fällig, überfällig oder als dringend markiert)
|
|
69
|
+
3. **Hohe Priorität**: Wichtige Aufgaben, die bald erledigt werden sollten
|
|
70
|
+
4. **Normale Priorität**: Regelmäßige Aufgaben, die später geplant werden können
|
|
71
|
+
5. **Niedrige Priorität**: Aufgaben, die erledigt werden können, wenn Zeit vorhanden ist
|
|
72
|
+
6. **Empfehlungen**: Vorgeschlagene Maßnahmen oder Prioritäten für den kommenden Zeitraum
|
|
73
|
+
|
|
74
|
+
Verwende die ClickUp-Suchtools, um mir zugewiesene Aufgaben zu finden, und hole detaillierte Informationen über die wichtigsten mit getTaskById.`
|
|
75
|
+
: `Can you look into ClickUp and check what my current TODO's are? Please search for all open tasks assigned to me, analyze their content, and categorize them by apparent priority (urgent, high, normal, low). For each category, provide a brief summary of what needs to be done and highlight any due dates or important details from the task descriptions.
|
|
76
|
+
|
|
77
|
+
Please structure your response with:
|
|
78
|
+
1. **Summary**: Total number of tasks and overall priority distribution
|
|
79
|
+
2. **Urgent Tasks**: Tasks that need immediate attention (due today, overdue, or marked as urgent)
|
|
80
|
+
3. **High Priority**: Important tasks that should be addressed soon
|
|
81
|
+
4. **Normal Priority**: Regular tasks that can be scheduled for later
|
|
82
|
+
5. **Low Priority**: Tasks that can be done when time permits
|
|
83
|
+
6. **Recommendations**: Suggested actions or priorities for the upcoming period
|
|
84
|
+
|
|
85
|
+
Use the ClickUp search tools to find tasks assigned to me, and get detailed information about the most important ones using getTaskById.`
|
|
86
|
+
}
|
|
87
|
+
}];
|
|
88
|
+
return { messages };
|
|
89
|
+
});
|
|
90
|
+
if (config_1.CONFIG.mode === 'read-minimal') {
|
|
91
|
+
// Core task context tools for AI coding assistance
|
|
92
|
+
// Only getTaskById and searchTasks
|
|
93
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
94
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
95
|
+
}
|
|
96
|
+
else if (config_1.CONFIG.mode === 'read') {
|
|
97
|
+
// All read-only tools
|
|
98
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
99
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
100
|
+
(0, space_tools_1.registerSpaceTools)(server);
|
|
101
|
+
(0, space_resources_1.registerSpaceResources)(server);
|
|
102
|
+
(0, list_tools_1.registerListToolsRead)(server);
|
|
103
|
+
(0, time_tools_1.registerTimeToolsRead)(server);
|
|
104
|
+
(0, doc_tools_1.registerDocumentToolsRead)(server);
|
|
105
|
+
}
|
|
106
|
+
else if (config_1.CONFIG.mode === 'write') {
|
|
107
|
+
// All tools (full functionality)
|
|
108
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
109
|
+
(0, task_write_tools_1.registerTaskToolsWrite)(server, userData);
|
|
110
|
+
(0, search_tools_1.registerSearchTools)(server, userData);
|
|
111
|
+
(0, space_tools_1.registerSpaceTools)(server);
|
|
112
|
+
(0, space_resources_1.registerSpaceResources)(server);
|
|
113
|
+
(0, list_tools_1.registerListToolsRead)(server);
|
|
114
|
+
(0, list_tools_1.registerListToolsWrite)(server);
|
|
115
|
+
(0, time_tools_1.registerTimeToolsRead)(server);
|
|
116
|
+
(0, time_tools_1.registerTimeToolsWrite)(server);
|
|
117
|
+
(0, doc_tools_1.registerDocumentToolsRead)(server);
|
|
118
|
+
(0, doc_tools_1.registerDocumentToolsWrite)(server);
|
|
119
|
+
(0, admin_tools_1.registerAdminToolsWrite)(server);
|
|
120
|
+
}
|
|
121
|
+
return server;
|
|
122
|
+
}
|
|
123
|
+
// Initialize server with enhanced documentation and export
|
|
124
|
+
const serverPromise = initializeServer();
|
|
125
|
+
exports.serverPromise = serverPromise;
|
|
126
|
+
// Only connect to the transport if this file is being run directly (not imported)
|
|
127
|
+
// OR if not being imported by CLI (to support Claude Desktop's module loading)
|
|
128
|
+
const isCliMode = process.argv.some(arg => arg.includes('cli.ts') || arg.includes('cli.js'));
|
|
129
|
+
if (require.main === module || !isCliMode) {
|
|
130
|
+
// Start receiving messages on stdin and sending messages on stdout after initialization
|
|
131
|
+
serverPromise.then(() => {
|
|
132
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
133
|
+
server.connect(transport);
|
|
134
|
+
}).catch(console.error);
|
|
135
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register ClickUp space resources using resource templates for dynamic discovery
|
|
4
|
+
*/
|
|
5
|
+
export declare function registerSpaceResources(server: McpServer): void;
|
|
6
|
+
//# sourceMappingURL=space-resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"space-resources.d.ts","sourceRoot":"","sources":["../../src/resources/space-resources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAiBtF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAwFvD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSpaceResources = registerSpaceResources;
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const utils_1 = require("../shared/utils");
|
|
6
|
+
/**
|
|
7
|
+
* Extract space ID from clickup:// URI
|
|
8
|
+
*/
|
|
9
|
+
function extractSpaceIdFromUri(uriString) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(uriString);
|
|
12
|
+
const pathParts = url.pathname.split('/');
|
|
13
|
+
return pathParts[pathParts.length - 1];
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new Error(`Invalid ClickUp space URI: ${uriString}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register ClickUp space resources using resource templates for dynamic discovery
|
|
21
|
+
*/
|
|
22
|
+
function registerSpaceResources(server) {
|
|
23
|
+
// Create resource template for ClickUp spaces
|
|
24
|
+
const spaceTemplate = new mcp_js_1.ResourceTemplate("clickup://space/{spaceId}", {
|
|
25
|
+
list: async () => {
|
|
26
|
+
try {
|
|
27
|
+
const searchIndex = await (0, utils_1.getSpaceSearchIndex)();
|
|
28
|
+
if (!searchIndex) {
|
|
29
|
+
return { resources: [] };
|
|
30
|
+
}
|
|
31
|
+
const spaces = searchIndex._docs || [];
|
|
32
|
+
// Filter out archived spaces for resource listing
|
|
33
|
+
const activeSpaces = spaces.filter((space) => !space.archived);
|
|
34
|
+
return {
|
|
35
|
+
resources: activeSpaces.map((space) => ({
|
|
36
|
+
uri: `clickup://space/${space.id}`,
|
|
37
|
+
name: `${space.name} ClickUp Space.txt`,
|
|
38
|
+
title: `${space.name} ClickUp Space`,
|
|
39
|
+
mimeType: "text/plain"
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error("Error listing space resources:", error);
|
|
45
|
+
return { resources: [] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Register resource template for ClickUp spaces
|
|
50
|
+
server.registerResource("clickup-spaces", spaceTemplate, {
|
|
51
|
+
title: "ClickUp Spaces",
|
|
52
|
+
description: "Access ClickUp spaces with their complete structure including lists, folders, and documents",
|
|
53
|
+
}, async (uri) => {
|
|
54
|
+
try {
|
|
55
|
+
const spaceId = extractSpaceIdFromUri(uri.toString());
|
|
56
|
+
// Fetch space content including lists, folders, and documents
|
|
57
|
+
const { lists, folders, documents } = await (0, utils_1.getSpaceContent)(spaceId);
|
|
58
|
+
// Get space details from the search index
|
|
59
|
+
const searchIndex = await (0, utils_1.getSpaceSearchIndex)();
|
|
60
|
+
const spaces = searchIndex._docs || [];
|
|
61
|
+
const space = spaces.find((s) => s.id === spaceId);
|
|
62
|
+
if (!space) {
|
|
63
|
+
return {
|
|
64
|
+
contents: [{
|
|
65
|
+
uri: uri.toString(),
|
|
66
|
+
text: `Space with ID ${spaceId} not found.`,
|
|
67
|
+
}]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Format the content using the shared tree formatting function
|
|
71
|
+
const treeContent = (0, utils_1.formatSpaceTree)(space, lists, folders, documents);
|
|
72
|
+
// Add resource metadata
|
|
73
|
+
const metadata = [
|
|
74
|
+
'\n---',
|
|
75
|
+
`ℹ️ Resource last updated: ${new Date().toISOString()}`,
|
|
76
|
+
`💡 For real-time data, use the searchSpaces tool`
|
|
77
|
+
].join('\n');
|
|
78
|
+
return {
|
|
79
|
+
contents: [{
|
|
80
|
+
uri: uri.toString(),
|
|
81
|
+
text: treeContent + metadata,
|
|
82
|
+
}]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error("Error reading space resource:", error);
|
|
87
|
+
return {
|
|
88
|
+
contents: [{
|
|
89
|
+
uri: uri.toString(),
|
|
90
|
+
text: `Error reading space: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
91
|
+
}]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const rawPrimaryLang: string | undefined;
|
|
2
|
+
export type McpMode = 'read-minimal' | 'read' | 'write';
|
|
3
|
+
export declare const CONFIG: {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
teamId: string;
|
|
6
|
+
maxImages: number;
|
|
7
|
+
maxResponseSizeMB: number;
|
|
8
|
+
primaryLanguageHint: string | undefined;
|
|
9
|
+
mode: McpMode;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIG = exports.rawPrimaryLang = void 0;
|
|
4
|
+
exports.rawPrimaryLang = process.env.CLICKUP_PRIMARY_LANGUAGE || process.env.LANG;
|
|
5
|
+
let detectedLanguageHint = undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced language detection that handles various formats and common language names
|
|
8
|
+
*/
|
|
9
|
+
function detectLanguage(rawLang) {
|
|
10
|
+
if (!rawLang)
|
|
11
|
+
return undefined;
|
|
12
|
+
const normalizedLang = rawLang.toLowerCase().trim();
|
|
13
|
+
// German language detection
|
|
14
|
+
if (normalizedLang === 'de' || normalizedLang === 'german' || normalizedLang === 'deutsch' || normalizedLang.startsWith('de_') || normalizedLang.startsWith('de-')) {
|
|
15
|
+
return 'de';
|
|
16
|
+
}
|
|
17
|
+
// English language detection
|
|
18
|
+
if (normalizedLang === 'en' || normalizedLang === 'english' || normalizedLang.startsWith('en_') || normalizedLang.startsWith('en-')) {
|
|
19
|
+
return 'en';
|
|
20
|
+
}
|
|
21
|
+
// French language detection
|
|
22
|
+
if (normalizedLang === 'fr' || normalizedLang === 'french' || normalizedLang === 'français' || normalizedLang.startsWith('fr_') || normalizedLang.startsWith('fr-')) {
|
|
23
|
+
return 'fr';
|
|
24
|
+
}
|
|
25
|
+
// Spanish language detection
|
|
26
|
+
if (normalizedLang === 'es' || normalizedLang === 'spanish' || normalizedLang === 'español' || normalizedLang.startsWith('es_') || normalizedLang.startsWith('es-')) {
|
|
27
|
+
return 'es';
|
|
28
|
+
}
|
|
29
|
+
// Italian language detection
|
|
30
|
+
if (normalizedLang === 'it' || normalizedLang === 'italian' || normalizedLang === 'italiano' || normalizedLang.startsWith('it_') || normalizedLang.startsWith('it-')) {
|
|
31
|
+
return 'it';
|
|
32
|
+
}
|
|
33
|
+
// Fallback: extract the primary language part (e.g., 'en' from 'en_US.UTF-8' or 'en-GB')
|
|
34
|
+
const langPart = normalizedLang.match(/^[a-zA-Z]{2,3}/);
|
|
35
|
+
if (langPart) {
|
|
36
|
+
return langPart[0].toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (exports.rawPrimaryLang) {
|
|
41
|
+
detectedLanguageHint = detectLanguage(exports.rawPrimaryLang);
|
|
42
|
+
}
|
|
43
|
+
const rawMode = process.env.CLICKUP_MCP_MODE?.toLowerCase();
|
|
44
|
+
let mcpMode = 'write'; // Default to write (full functionality)
|
|
45
|
+
if (rawMode === 'read-minimal' || rawMode === 'read') {
|
|
46
|
+
mcpMode = rawMode;
|
|
47
|
+
}
|
|
48
|
+
else if (rawMode && rawMode !== 'write') {
|
|
49
|
+
console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
|
|
50
|
+
}
|
|
51
|
+
exports.CONFIG = {
|
|
52
|
+
apiKey: process.env.CLICKUP_API_KEY,
|
|
53
|
+
teamId: process.env.CLICKUP_TEAM_ID,
|
|
54
|
+
maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
|
|
55
|
+
maxResponseSizeMB: process.env.MAX_RESPONSE_SIZE_MB ? parseFloat(process.env.MAX_RESPONSE_SIZE_MB) : 1,
|
|
56
|
+
primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
|
|
57
|
+
mode: mcpMode,
|
|
58
|
+
};
|
|
59
|
+
if (!exports.CONFIG.apiKey || !exports.CONFIG.teamId) {
|
|
60
|
+
throw new Error("Missing Clickup API key or team ID");
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ParsedDataUri {
|
|
2
|
+
mimeType: string;
|
|
3
|
+
base64Data: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse data URI strings of the form data:mime/type;base64,....
|
|
7
|
+
* Returns null if the value is not a base64 data URI.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseDataUri(dataUri: string): ParsedDataUri | null;
|
|
10
|
+
/**
|
|
11
|
+
* Estimate the decoded byte-size of base64 data without allocating buffers
|
|
12
|
+
*/
|
|
13
|
+
export declare function estimateBase64Size(base64Data: string): number;
|
|
14
|
+
//# sourceMappingURL=data-uri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-uri.d.ts","sourceRoot":"","sources":["../../src/shared/data-uri.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseDataUri = parseDataUri;
|
|
4
|
+
exports.estimateBase64Size = estimateBase64Size;
|
|
5
|
+
/**
|
|
6
|
+
* Parse data URI strings of the form data:mime/type;base64,....
|
|
7
|
+
* Returns null if the value is not a base64 data URI.
|
|
8
|
+
*/
|
|
9
|
+
function parseDataUri(dataUri) {
|
|
10
|
+
if (!dataUri.startsWith("data:")) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const match = dataUri.match(/^data:([^;]+);base64,(.+)$/s);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const [, mimeType, base64Part] = match;
|
|
18
|
+
const sanitizedBase64 = base64Part.replace(/\s+/g, "");
|
|
19
|
+
if (!sanitizedBase64) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
mimeType,
|
|
24
|
+
base64Data: sanitizedBase64,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Estimate the decoded byte-size of base64 data without allocating buffers
|
|
29
|
+
*/
|
|
30
|
+
function estimateBase64Size(base64Data) {
|
|
31
|
+
const sanitized = base64Data.replace(/\s+/g, "");
|
|
32
|
+
const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
|
|
33
|
+
return Math.max(0, (sanitized.length * 3) / 4 - padding);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ContentBlock, ImageMetadataBlock } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Downloads images from image_metadata blocks and applies smart size/count limiting
|
|
4
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
5
|
+
* Uses intelligent size calculation accounting for text content
|
|
6
|
+
*
|
|
7
|
+
* @param content Array of content blocks that may contain image_metadata blocks
|
|
8
|
+
* @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
|
|
9
|
+
* @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
|
|
10
|
+
* @returns Promise resolving to content array with downloaded images or placeholders
|
|
11
|
+
*/
|
|
12
|
+
export declare function downloadImages(content: (ContentBlock | ImageMetadataBlock)[], maxImages?: number, maxSizeMB?: number): Promise<ContentBlock[]>;
|
|
13
|
+
//# sourceMappingURL=image-processing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-processing.d.ts","sourceRoot":"","sources":["../../src/shared/image-processing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,SAAS,CAAC;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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadImages = downloadImages;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const data_uri_1 = require("./data-uri");
|
|
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
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Downloads images from image_metadata blocks and applies smart size/count limiting
|
|
49
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
50
|
+
* Uses intelligent size calculation accounting for text content
|
|
51
|
+
*
|
|
52
|
+
* @param content Array of content blocks that may contain image_metadata blocks
|
|
53
|
+
* @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
|
|
54
|
+
* @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
|
|
55
|
+
* @returns Promise resolving to content array with downloaded images or placeholders
|
|
56
|
+
*/
|
|
57
|
+
async function downloadImages(content, maxImages = config_1.CONFIG.maxImages, maxSizeMB = config_1.CONFIG.maxResponseSizeMB) {
|
|
58
|
+
// First apply count-based limiting to image_metadata blocks
|
|
59
|
+
const countLimitedContent = applyCountBasedLimitToImageMetadata(content, maxImages);
|
|
60
|
+
// Calculate text size to determine available image budget
|
|
61
|
+
const textContent = countLimitedContent.filter(block => block.type !== "image_metadata");
|
|
62
|
+
const textSizeBytes = JSON.stringify(textContent).length;
|
|
63
|
+
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
64
|
+
const availableImageBudget = Math.max(0, maxSizeBytes - textSizeBytes);
|
|
65
|
+
// Calculate per-image budget
|
|
66
|
+
const imageMetadataBlocks = countLimitedContent.filter(block => block.type === "image_metadata");
|
|
67
|
+
const perImageBudget = imageMetadataBlocks.length > 0 ? availableImageBudget / imageMetadataBlocks.length : 0;
|
|
68
|
+
// Download images in parallel and replace image_metadata blocks
|
|
69
|
+
const downloadPromises = countLimitedContent.map(async (block) => {
|
|
70
|
+
if (block.type === "image_metadata") {
|
|
71
|
+
if (block.inlineData) {
|
|
72
|
+
return convertInlineImage(block, perImageBudget);
|
|
73
|
+
}
|
|
74
|
+
return await downloadSingleImage(block, perImageBudget);
|
|
75
|
+
}
|
|
76
|
+
return block;
|
|
77
|
+
});
|
|
78
|
+
return Promise.all(downloadPromises);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Apply count-based limiting to image_metadata blocks
|
|
82
|
+
* Keeps the most recent image_metadata blocks (newest last)
|
|
83
|
+
*/
|
|
84
|
+
function applyCountBasedLimitToImageMetadata(content, maxImages) {
|
|
85
|
+
// Find all image_metadata block indices
|
|
86
|
+
const imageMetadataIndices = [];
|
|
87
|
+
content.forEach((block, index) => {
|
|
88
|
+
if (block.type === "image_metadata") {
|
|
89
|
+
imageMetadataIndices.push(index);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// If we have fewer images than the limit, return the original content
|
|
93
|
+
if (imageMetadataIndices.length <= maxImages) {
|
|
94
|
+
return content;
|
|
95
|
+
}
|
|
96
|
+
// Determine which image_metadata blocks to remove (keep the most recent ones)
|
|
97
|
+
const imagesToRemove = imageMetadataIndices.slice(0, imageMetadataIndices.length - maxImages);
|
|
98
|
+
// Create a new content array with excess image_metadata blocks replaced by text placeholders
|
|
99
|
+
return content.map((block, index) => {
|
|
100
|
+
if (block.type === "image_metadata" && imagesToRemove.includes(index)) {
|
|
101
|
+
return {
|
|
102
|
+
type: "text",
|
|
103
|
+
text: "[Image removed due to count limitations. Only the most recent images are shown.]",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return block;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Download a single image from an image_metadata block, trying different sizes if needed
|
|
111
|
+
*/
|
|
112
|
+
async function downloadSingleImage(imageMetadata, perImageBudget) {
|
|
113
|
+
const fallbackText = createImageFallback(imageMetadata);
|
|
114
|
+
// Try each URL in order (largest to smallest)
|
|
115
|
+
for (const url of imageMetadata.urls) {
|
|
116
|
+
const abortController = new AbortController();
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(url, {
|
|
119
|
+
signal: abortController.signal
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
console.error(`Failed to fetch image from ${url}: ${response.status}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Check Content-Length header first to avoid downloading large images
|
|
126
|
+
const contentLength = response.headers.get("Content-Length");
|
|
127
|
+
if (contentLength) {
|
|
128
|
+
const imageSizeBytes = parseInt(contentLength, 10);
|
|
129
|
+
if (imageSizeBytes > perImageBudget) {
|
|
130
|
+
// Cancel the request to stop any ongoing download
|
|
131
|
+
abortController.abort();
|
|
132
|
+
console.error(`Image from ${url} is ${imageSizeBytes} bytes (from Content-Length), exceeds budget of ${perImageBudget} bytes`);
|
|
133
|
+
// Continue to try smaller thumbnail
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const imageBuffer = await response.arrayBuffer();
|
|
138
|
+
const actualSizeBytes = imageBuffer.byteLength;
|
|
139
|
+
// Double-check actual size (in case Content-Length was missing or incorrect)
|
|
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";
|
|
144
|
+
return {
|
|
145
|
+
type: "image",
|
|
146
|
+
mimeType,
|
|
147
|
+
data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Cancel to clean up the connection
|
|
152
|
+
abortController.abort();
|
|
153
|
+
console.error(`Image from ${url} is ${actualSizeBytes} bytes (actual size), exceeds budget of ${perImageBudget} bytes`);
|
|
154
|
+
// Continue to try smaller thumbnail
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.name === 'AbortError') {
|
|
159
|
+
// Request was cancelled, this is expected - continue to next URL
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
console.error(`Error fetching image from ${url}: ${error.message || "Unknown error"}`);
|
|
163
|
+
// Continue to try next URL
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// If all URLs failed or were too large, return fallback text
|
|
167
|
+
return fallbackText;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a fallback text block when an image cannot be included
|
|
171
|
+
*/
|
|
172
|
+
function createImageFallback(imageMetadata) {
|
|
173
|
+
return {
|
|
174
|
+
type: "text",
|
|
175
|
+
text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Convert inline image data URIs into image blocks while respecting size budgets
|
|
180
|
+
*/
|
|
181
|
+
function convertInlineImage(imageMetadata, perImageBudget) {
|
|
182
|
+
const inlineData = imageMetadata.inlineData;
|
|
183
|
+
if (!inlineData) {
|
|
184
|
+
return createImageFallback(imageMetadata);
|
|
185
|
+
}
|
|
186
|
+
const estimatedSize = (0, data_uri_1.estimateBase64Size)(inlineData.base64Data);
|
|
187
|
+
if (perImageBudget <= 0 || estimatedSize > perImageBudget) {
|
|
188
|
+
console.error(`Inline image for "${imageMetadata.alt}" is ${estimatedSize} bytes, exceeds budget of ${perImageBudget} bytes`);
|
|
189
|
+
return createImageFallback(imageMetadata);
|
|
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";
|
|
194
|
+
return {
|
|
195
|
+
type: "image",
|
|
196
|
+
mimeType,
|
|
197
|
+
data: inlineData.base64Data,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
|
+
import { ParsedDataUri } from "./data-uri";
|
|
3
|
+
export type ContentBlock = CallToolResult['content'][number];
|
|
4
|
+
export interface ImageMetadataBlock {
|
|
5
|
+
type: "image_metadata";
|
|
6
|
+
urls: string[];
|
|
7
|
+
alt: string;
|
|
8
|
+
inlineData?: ParsedDataUri;
|
|
9
|
+
}
|
|
10
|
+
export interface DatedContentEvent {
|
|
11
|
+
date: string;
|
|
12
|
+
contentBlocks: (ContentBlock | ImageMetadataBlock)[];
|
|
13
|
+
}
|
|
14
|
+
export interface Config {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
teamId: string;
|
|
17
|
+
maxImages: number;
|
|
18
|
+
maxResponseSizeMB: number;
|
|
19
|
+
primaryLanguageHint: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAGD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,CAAC;CACtD;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC"}
|