@hauptsache.net/clickup-mcp 1.0.5 → 1.1.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/dist/index.js CHANGED
@@ -1,478 +1,64 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.server = void 0;
4
+ exports.serverPromise = exports.server = void 0;
8
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
6
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
- const zod_1 = require("zod");
11
- const clickup_text_1 = require("./clickup-text");
12
- const fuse_js_1 = __importDefault(require("fuse.js"));
13
- // --- End Simplified Event Wrapper ---
14
- const rawPrimaryLang = process.env.CLICKUP_PRIMARY_LANGUAGE || process.env.LANG;
15
- let detectedLanguageHint = undefined;
16
- if (rawPrimaryLang) {
17
- // Extract the primary language part (e.g., 'en' from 'en_US.UTF-8' or 'en-GB')
18
- // and convert to lowercase.
19
- const langPart = rawPrimaryLang.match(/^[a-zA-Z]{2,3}/);
20
- if (langPart) {
21
- detectedLanguageHint = langPart[0].toLowerCase();
22
- }
23
- }
24
- const CONFIG = {
25
- apiKey: process.env.CLICKUP_API_KEY,
26
- teamId: process.env.CLICKUP_TEAM_ID,
27
- maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
28
- primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
29
- };
30
- if (!CONFIG.apiKey || !CONFIG.teamId) {
31
- throw new Error("Missing Clickup API key or team ID");
32
- }
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 search_tools_1 = require("./tools/search-tools");
12
+ const space_tools_1 = require("./tools/space-tools");
13
+ const list_tools_1 = require("./tools/list-tools");
14
+ const time_tools_1 = require("./tools/time-tools");
33
15
  // Create an MCP server
34
- exports.server = new mcp_js_1.McpServer({
16
+ const server = new mcp_js_1.McpServer({
35
17
  name: "Clickup MCP",
36
18
  version: "1.0.0",
37
19
  });
38
- /**
39
- * Checks if a string looks like a valid ClickUp task ID
40
- * Valid task IDs are 7-9 characters long and contain only alphanumeric characters
41
- */
42
- function isTaskId(str) {
43
- // Task IDs are 7-9 characters long and contain only alphanumeric characters
44
- return /^[a-z0-9]{7,9}$/i.test(str);
45
- }
46
- exports.server.tool("getTaskById", "Get a Clickup task with images and comments by ID", {
47
- id: zod_1.z
48
- .string()
49
- .min(7)
50
- .max(9)
51
- .refine(val => isTaskId(val), {
52
- message: "Task ID must be 7-9 alphanumeric characters only"
53
- })
54
- .describe(`The 7-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
55
- }, async ({ id }) => {
56
- // 1. Load base task content, comment events, and status change events in parallel
57
- const [taskDetailContentBlocks, commentEvents, statusChangeEvents] = await Promise.all([
58
- loadTaskContent(id), // Returns Promise<ContentBlock[]>
59
- loadTaskComments(id), // Returns Promise<DatedContentEvent[]>
60
- loadTimeInStatusHistory(id), // Returns Promise<DatedContentEvent[]>
61
- ]);
62
- // 2. Combine comment and status change events
63
- const allDatedEvents = [...commentEvents, ...statusChangeEvents];
64
- // 3. Sort all dated events chronologically
65
- allDatedEvents.sort((a, b) => {
66
- const dateA = a.date ? parseInt(a.date) : 0;
67
- const dateB = b.date ? parseInt(b.date) : 0;
68
- return dateA - dateB;
69
- });
70
- // 4. Flatten sorted events into a single ContentBlock stream
71
- let processedEventBlocks = [];
72
- for (const event of allDatedEvents) {
73
- processedEventBlocks.push(...event.contentBlocks);
74
- }
75
- // 5. Combine task details with processed event blocks
76
- const allContentBlocks = [...taskDetailContentBlocks, ...processedEventBlocks];
77
- // 6. Limit images
78
- const limitedContent = limitImages(allContentBlocks, CONFIG.maxImages);
79
- return {
80
- content: limitedContent,
81
- };
82
- });
83
- async function loadTaskContent(taskId) {
84
- const response = await fetch(`https://api.clickup.com/api/v2/task/${taskId}?include_markdown_description=true&include_subtasks=true`, { headers: { Authorization: CONFIG.apiKey } });
85
- const task = await response.json();
86
- const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
87
- // Create the task metadata block using the helper function
88
- const taskMetadata = await generateTaskMetadata(task);
89
- return [taskMetadata, ...content];
90
- }
91
- async function loadTaskComments(id) {
92
- const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0`, // Ensure all comments are fetched
93
- { headers: { Authorization: CONFIG.apiKey } });
94
- if (!response.ok) {
95
- console.error(`Error fetching comments for task ${id}: ${response.status} ${response.statusText}`);
96
- return [];
97
- }
98
- const commentsData = await response.json();
99
- if (!commentsData.comments || !Array.isArray(commentsData.comments)) {
100
- console.error(`Unexpected comment data structure for task ${id}`);
101
- return [];
102
- }
103
- const commentEvents = await Promise.all(commentsData.comments.map(async (comment) => {
104
- const headerBlock = {
105
- type: "text",
106
- text: `Comment by ${comment.user.username} on ${new Date(+comment.date)}:`,
107
- };
108
- const commentBodyBlocks = await (0, clickup_text_1.processClickUpText)(comment.comment);
109
- return {
110
- date: comment.date, // String timestamp from ClickUp for sorting
111
- contentBlocks: [headerBlock, ...commentBodyBlocks],
112
- };
113
- }));
114
- return commentEvents;
115
- }
116
- async function loadTimeInStatusHistory(taskId) {
117
- const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status`;
118
- try {
119
- const response = await fetch(url, { headers: { Authorization: CONFIG.apiKey } });
120
- if (!response.ok) {
121
- console.error(`Error fetching time in status for task ${taskId}: ${response.status} ${response.statusText}`);
122
- return [];
123
- }
124
- // Using 'any' for less strict typing as per user preference, but keeping structure for clarity
125
- const data = await response.json();
126
- const events = [];
127
- const processStatusEntry = (entry) => {
128
- if (!entry || !entry.total_time || !entry.total_time.since || !entry.status)
129
- return null;
130
- return {
131
- date: entry.total_time.since,
132
- contentBlocks: [{
133
- type: "text",
134
- text: `Status set to '${entry.status}' on ${new Date(+entry.total_time.since)}`,
135
- }],
136
- };
137
- };
138
- if (data.status_history && Array.isArray(data.status_history)) {
139
- data.status_history.forEach((historyEntry) => {
140
- const event = processStatusEntry(historyEntry);
141
- if (event)
142
- events.push(event);
143
- });
144
- }
145
- if (data.current_status) {
146
- const event = processStatusEntry(data.current_status);
147
- // Ensure current_status is only added if it's distinct or more recent than the last history item.
148
- // The deduplication logic below handles if it's the same as the last history entry.
149
- if (event)
150
- events.push(event);
151
- }
152
- // Deduplicate events based on date and status name to avoid adding current_status if it's identical to the last history entry
153
- const uniqueEvents = Array.from(new Map(events.map(event => [`${event.date}-${event.contentBlocks[0]?.text}`, event] // Keying by date and text content of first block
154
- )).values());
155
- return uniqueEvents;
156
- }
157
- catch (error) {
158
- console.error(`Exception fetching time in status for task ${taskId}:`, error);
159
- return [];
160
- }
161
- }
162
- /**
163
- * Helper function to generate consistent task metadata
164
- */
165
- async function generateTaskMetadata(task) {
166
- let spaceName = task.space?.name || 'Unknown Space';
167
- let spaceIdForDisplay = task.space?.id || 'N/A';
168
- if (spaceName === 'Unknown Space' && task.space?.id) {
169
- const spaceDetails = await getSpaceDetails(task.space.id);
170
- if (spaceDetails && spaceDetails.name) {
171
- spaceName = spaceDetails.name;
172
- }
173
- }
174
- const metadataLines = [
175
- `task_id: ${task.id}`,
176
- `name: ${task.name}`,
177
- `status: ${task.status.status}`,
178
- `date_created: ${new Date(+task.date_created)}`,
179
- `date_updated: ${new Date(+task.date_updated)}`,
180
- `creator: ${task.creator.username}`,
181
- `assignee: ${task.assignees.map((a) => a.username).join(', ')}`,
182
- `list: ${task.list.name} (${task.list.id})`,
183
- `space: ${spaceName} (${spaceIdForDisplay})`,
184
- ];
185
- // Add priority if it exists
186
- if (task.priority !== undefined && task.priority !== null) {
187
- const priorityName = task.priority.priority || 'none';
188
- metadataLines.push(`priority: ${priorityName}`);
189
- }
190
- // Add due date if it exists
191
- if (task.due_date) {
192
- metadataLines.push(`due_date: ${new Date(+task.due_date)}`);
193
- }
194
- // Add start date if it exists
195
- if (task.start_date) {
196
- metadataLines.push(`start_date: ${new Date(+task.start_date)}`);
197
- }
198
- // Add time estimate if it exists
199
- if (task.time_estimate) {
200
- const hours = Math.floor(task.time_estimate / 3600000);
201
- const minutes = Math.floor((task.time_estimate % 3600000) / 60000);
202
- metadataLines.push(`time_estimate: ${hours}h ${minutes}m`);
203
- }
204
- // Add tags if they exist
205
- if (task.tags && task.tags.length > 0) {
206
- metadataLines.push(`tags: ${task.tags.map((t) => t.name).join(', ')}`);
207
- }
208
- // Add watchers if they exist
209
- if (task.watchers && task.watchers.length > 0) {
210
- metadataLines.push(`watchers: ${task.watchers.map((w) => w.username).join(', ')}`);
211
- }
212
- // Add parent task information if it exists
213
- if (typeof task.parent === "string") {
214
- metadataLines.push(`parent_task_id: ${task.parent}`);
215
- }
216
- // Add child task information if it exists
217
- if (task.subtasks && task.subtasks.length > 0) {
218
- metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
219
- }
220
- // Add task URL
221
- metadataLines.push(`url: ${task.url}`);
222
- // Add archived status if true
223
- if (task.archived) {
224
- metadataLines.push(`archived: true`);
225
- }
226
- // Add custom fields if they exist
227
- if (task.custom_fields && task.custom_fields.length > 0) {
228
- task.custom_fields.forEach((field) => {
229
- if (field.value !== undefined && field.value !== null && field.value !== '') {
230
- const fieldName = field.name.toLowerCase().replace(/\s+/g, '_');
231
- let fieldValue = field.value;
232
- // Handle different custom field types
233
- if (field.type === 'drop_down' && typeof field.value === 'number') {
234
- // For dropdown fields, find the selected option
235
- const selectedOption = field.type_config?.options?.find((opt) => opt.orderindex === field.value);
236
- fieldValue = selectedOption?.name || field.value;
237
- }
238
- else if (Array.isArray(field.value)) {
239
- // For multi-select or array values
240
- fieldValue = field.value.map((v) => v.name || v).join(', ');
241
- }
242
- else if (typeof field.value === 'object') {
243
- // For object values (like users), extract meaningful data
244
- fieldValue = field.value.username || field.value.name || JSON.stringify(field.value);
245
- }
246
- metadataLines.push(`custom_${fieldName}: ${fieldValue}`);
247
- }
248
- });
249
- }
250
- return {
251
- type: "text",
252
- text: metadataLines.join("\n"),
253
- };
254
- }
255
- const spaceCache = new Map(); // Global cache for space details promises
256
- /**
257
- * Function to get space details, using a cache to avoid redundant fetches
258
- */
259
- async function getSpaceDetails(spaceId) {
260
- if (!spaceId) {
261
- return null;
262
- }
263
- if (!spaceCache.has(spaceId)) {
264
- const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, {
265
- headers: { Authorization: CONFIG.apiKey },
266
- })
267
- .then(res => {
268
- if (!res.ok) {
269
- // Don't cache failed requests, or handle errors more gracefully
270
- console.error(`Error fetching space ${spaceId}: ${res.status}`);
271
- spaceCache.delete(spaceId); // Allow retry on next call
272
- return null;
273
- }
274
- return res.json();
275
- })
276
- .catch(error => {
277
- console.error(`Network error fetching space ${spaceId}:`, error);
278
- spaceCache.delete(spaceId); // Allow retry on next call
279
- return null;
280
- });
281
- spaceCache.set(spaceId, fetchPromise);
282
- }
283
- return spaceCache.get(spaceId);
284
- }
285
- /**
286
- * Limits the number of images in the content array, replacing excess images with text placeholders
287
- * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
288
- *
289
- * @param content Array of content blocks that may contain images
290
- * @param maxImages Maximum number of images to keep
291
- * @returns Modified content array with limited images
292
- */
293
- function limitImages(content, maxImages) {
294
- // Count how many images we have
295
- const imageIndices = [];
296
- // Find all image blocks
297
- content.forEach((block, index) => {
298
- if (block.type === "image") {
299
- imageIndices.push(index);
300
- }
301
- });
302
- // If we have fewer images than the limit, return the original content
303
- if (imageIndices.length <= maxImages) {
304
- return content;
305
- }
306
- // Determine which images to keep (the most recent ones)
307
- // We want to keep the last 'maxImages' images
308
- const imagesToRemove = imageIndices.slice(0, imageIndices.length - maxImages);
309
- // Create a new content array with excess images replaced by text
310
- return content.map((block, index) => {
311
- if (block.type === "image" && imagesToRemove.includes(index)) {
312
- return {
313
- type: "text",
314
- text: "[Image removed due to size limitations. Only the most recent images are shown.]",
315
- };
316
- }
317
- return block;
318
- });
20
+ exports.server = 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 for enhanced tool documentation and API health check
25
+ const userData = await (0, utils_1.getCurrentUser)();
26
+ console.error(`Connected as: ${userData.user.username} (${userData.user.email})`);
27
+ if (config_1.CONFIG.mode === 'read-minimal') {
28
+ // Core task context tools for AI coding assistance
29
+ // Only getTaskById and searchTasks
30
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
31
+ (0, search_tools_1.registerSearchTools)(server);
32
+ }
33
+ else if (config_1.CONFIG.mode === 'read') {
34
+ // All read-only tools
35
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
36
+ (0, search_tools_1.registerSearchTools)(server);
37
+ (0, space_tools_1.registerSpaceTools)(server);
38
+ (0, list_tools_1.registerListToolsRead)(server);
39
+ (0, time_tools_1.registerTimeToolsRead)(server);
40
+ }
41
+ else if (config_1.CONFIG.mode === 'write') {
42
+ // All tools (full functionality)
43
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
44
+ (0, task_tools_1.registerTaskToolsWrite)(server, userData);
45
+ (0, search_tools_1.registerSearchTools)(server);
46
+ (0, space_tools_1.registerSpaceTools)(server);
47
+ (0, list_tools_1.registerListToolsRead)(server);
48
+ (0, list_tools_1.registerListToolsWrite)(server);
49
+ (0, time_tools_1.registerTimeToolsRead)(server);
50
+ (0, time_tools_1.registerTimeToolsWrite)(server);
51
+ }
52
+ return server;
319
53
  }
320
- let taskSearchIndex = null;
321
- let lastIndexUpdateTime = 0;
322
- const INDEX_REFRESH_INTERVAL = 60000; // 60 seconds
323
- const MAX_SEARCH_RESULTS = 50;
324
- // Dynamically construct the searchTask description
325
- const searchTaskDescriptionBase = [
326
- "Searches tasks by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
327
- // Placeholder for language-specific guidance
328
- "You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
329
- "Always use getTaskById to get more specific information if a task is relevant.",
330
- ];
331
- if (CONFIG.primaryLanguageHint && CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
332
- searchTaskDescriptionBase.splice(1, 0, `For optimal results, as your ClickUp tasks may be primarily in '${CONFIG.primaryLanguageHint}', consider providing search terms in English and '${CONFIG.primaryLanguageHint}'.`);
333
- }
334
- exports.server.tool("searchTask", searchTaskDescriptionBase.join("\n"), {
335
- terms: zod_1.z
336
- .string()
337
- .min(3)
338
- .describe("Search terms separated by '|' for OR logic (e.g., 'term1|term2|term3') or a direct task ID"),
339
- }, async ({ terms }) => {
340
- const now = Date.now();
341
- if (!taskSearchIndex || (now - lastIndexUpdateTime > INDEX_REFRESH_INTERVAL)) {
342
- console.error('Refreshing ClickUp task index...');
343
- const taskListsPromises = [...Array(30)].map((_, i) => {
344
- return fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}&subtasks=true`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json()).catch(e => {
345
- console.error(`Error fetching page ${i} for index:`, e);
346
- return { tasks: [] };
347
- });
348
- });
349
- const taskLists = await Promise.all(taskListsPromises);
350
- const allFetchedTasks = taskLists.flatMap(taskList => taskList.tasks);
351
- if (allFetchedTasks.length > 0) {
352
- taskSearchIndex = new fuse_js_1.default(allFetchedTasks, {
353
- keys: [
354
- { name: 'name', weight: 0.7 },
355
- { name: 'id', weight: 0.6 },
356
- { name: 'text_content', weight: 0.5 }, // Task description/content
357
- { name: 'tags.name', weight: 0.4 }, // Task Tags
358
- { name: 'assignees.username', weight: 0.4 }, // Task Assignees
359
- { name: 'list.name', weight: 0.3 }, // Name of the List the task is in
360
- { name: 'folder.name', weight: 0.2 }, // Name of the Folder the task is in
361
- { name: 'space.name', weight: 0.1 } // Name of the Space the task is in
362
- ],
363
- includeScore: true,
364
- threshold: 0.4,
365
- minMatchCharLength: 2,
366
- });
367
- lastIndexUpdateTime = now;
368
- console.error(`Task index refreshed with ${allFetchedTasks.length} tasks.`);
369
- }
370
- else {
371
- console.error('No tasks fetched to build search index.');
372
- // Potentially keep the old index if fetching failed, or clear it
373
- // For now, if fetching fails to get any tasks, the index won't update.
374
- }
375
- }
376
- const searchTermsArray = terms
377
- .split("|")
378
- .map((term) => term.trim())
379
- .filter(term => term.length > 0);
380
- if (searchTermsArray.length === 0) {
381
- return { content: [{ type: "text", text: "No search terms provided." }] };
382
- }
383
- const uniqueResults = new Map();
384
- if (taskSearchIndex) {
385
- searchTermsArray.forEach(term => {
386
- const results = taskSearchIndex.search(term.toLowerCase());
387
- results.forEach(result => {
388
- if (result.item && typeof result.item.id === 'string') {
389
- const currentScore = result.score ?? 1; // Default to 1 if undefined
390
- const existing = uniqueResults.get(result.item.id);
391
- if (!existing || currentScore < existing.score) {
392
- uniqueResults.set(result.item.id, { item: result.item, score: currentScore });
393
- }
394
- }
395
- });
396
- });
397
- }
398
- // Task ID Fallback Logic
399
- const potentialTaskIds = searchTermsArray.filter(isTaskId);
400
- const foundTaskIdsByFuse = new Set(Array.from(uniqueResults.keys()).map(id => id.toLowerCase())); // Store lowercase for comparison
401
- // Filter task IDs that were not found by Fuse, comparing case-insensitively
402
- const taskIdsToFetchDirectly = potentialTaskIds.filter(id => {
403
- const lowerId = id.toLowerCase();
404
- return !foundTaskIdsByFuse.has(lowerId);
405
- });
406
- if (taskIdsToFetchDirectly.length > 0) {
407
- console.error(`Attempting direct fetch for task IDs: ${taskIdsToFetchDirectly.join(', ')}`);
408
- const directFetchPromises = taskIdsToFetchDirectly.map(async (id) => {
409
- try {
410
- const response = await fetch(`https://api.clickup.com/api/v2/task/${id}`, { headers: { Authorization: CONFIG.apiKey } });
411
- if (response.ok) {
412
- const task = await response.json();
413
- if (task && typeof task.id === 'string') {
414
- // Add/update with a perfect score if fetched directly, unless a better Fuse score already exists
415
- const existing = uniqueResults.get(task.id);
416
- if (!existing || 0 < existing.score) { // 0 is a perfect score
417
- uniqueResults.set(task.id, { item: task, score: 0 });
418
- }
419
- }
420
- return task;
421
- }
422
- return null;
423
- }
424
- catch (error) {
425
- console.error(`Error directly fetching task ${id}:`, error);
426
- return null;
427
- }
428
- });
429
- await Promise.all(directFetchPromises);
430
- }
431
- const sortedResults = Array.from(uniqueResults.values())
432
- .sort((a, b) => a.score - b.score) // Sort by score, ascending (lower is better)
433
- .map(entry => entry.item)
434
- .slice(0, MAX_SEARCH_RESULTS); // Limit the number of results
435
- if (sortedResults.length === 0) {
436
- return {
437
- content: [
438
- {
439
- type: "text",
440
- text: "No tasks found matching the search terms.",
441
- },
442
- ],
443
- };
444
- }
445
- return {
446
- content: await Promise.all(sortedResults.map((task) => generateTaskMetadata(task))),
447
- };
448
- });
449
- exports.server.tool("listTodo", "Lists all open tasks for the current user.", async () => {
450
- // fetch current user ID
451
- const userResp = await fetch("https://api.clickup.com/api/v2/user", {
452
- headers: { Authorization: CONFIG.apiKey },
453
- }).then((res) => res.json());
454
- const userId = userResp.user.id;
455
- // page through team tasks assigned to this user
456
- const taskLists = await Promise.all([...Array(10)].map((_, i) => fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}&assignees[]=${userId}`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json())));
457
- const tasks = taskLists.flatMap((tl) => tl.tasks);
458
- // filter out closed tasks
459
- const openTasks = tasks
460
- .filter((task) => task.status.type !== "done") // done is not closed but also not a todo
461
- .slice(0, 50);
462
- if (openTasks.length === 0) {
463
- return {
464
- content: [
465
- { type: "text", text: "No open tasks found for the current user." },
466
- ],
467
- };
468
- }
469
- return {
470
- content: await Promise.all(openTasks.map((task) => generateTaskMetadata(task))),
471
- };
472
- });
54
+ // Initialize server with enhanced documentation and export
55
+ const serverPromise = initializeServer();
56
+ exports.serverPromise = serverPromise;
473
57
  // Only connect to the transport if this file is being run directly (not imported)
474
58
  if (require.main === module) {
475
- // Start receiving messages on stdin and sending messages on stdout
476
- const transport = new stdio_js_1.StdioServerTransport();
477
- exports.server.connect(transport);
59
+ // Start receiving messages on stdin and sending messages on stdout after initialization
60
+ serverPromise.then(() => {
61
+ const transport = new stdio_js_1.StdioServerTransport();
62
+ server.connect(transport);
63
+ }).catch(console.error);
478
64
  }
@@ -0,0 +1,10 @@
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
+ primaryLanguageHint: string | undefined;
8
+ mode: McpMode;
9
+ };
10
+ //# 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;AAavF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;CAMlB,CAAC"}
@@ -0,0 +1,31 @@
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
+ if (exports.rawPrimaryLang) {
7
+ // Extract the primary language part (e.g., 'en' from 'en_US.UTF-8' or 'en-GB')
8
+ // and convert to lowercase.
9
+ const langPart = exports.rawPrimaryLang.match(/^[a-zA-Z]{2,3}/);
10
+ if (langPart) {
11
+ detectedLanguageHint = langPart[0].toLowerCase();
12
+ }
13
+ }
14
+ const rawMode = process.env.CLICKUP_MCP_MODE?.toLowerCase();
15
+ let mcpMode = 'write'; // Default to write (full functionality)
16
+ if (rawMode === 'read-minimal' || rawMode === 'read') {
17
+ mcpMode = rawMode;
18
+ }
19
+ else if (rawMode && rawMode !== 'write') {
20
+ console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
21
+ }
22
+ exports.CONFIG = {
23
+ apiKey: process.env.CLICKUP_API_KEY,
24
+ teamId: process.env.CLICKUP_TEAM_ID,
25
+ maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
26
+ primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
27
+ mode: mcpMode,
28
+ };
29
+ if (!exports.CONFIG.apiKey || !exports.CONFIG.teamId) {
30
+ throw new Error("Missing Clickup API key or team ID");
31
+ }
@@ -0,0 +1,13 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
2
+ export type ContentBlock = CallToolResult['content'][number];
3
+ export interface DatedContentEvent {
4
+ date: string;
5
+ contentBlocks: ContentBlock[];
6
+ }
7
+ export interface Config {
8
+ apiKey: string;
9
+ teamId: string;
10
+ maxImages: number;
11
+ primaryLanguageHint: string | undefined;
12
+ }
13
+ //# 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;AAGjE,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,30 @@
1
+ import { ContentBlock } from "./types";
2
+ import Fuse from 'fuse.js';
3
+ /**
4
+ * Checks if a string looks like a valid ClickUp task ID
5
+ * Valid task IDs are 6-9 characters long and contain only alphanumeric characters
6
+ */
7
+ export declare function isTaskId(str: string): boolean;
8
+ /**
9
+ * Get current authenticated user information from ClickUp API
10
+ * Caches the result after first successful fetch to avoid repeated API calls during the session
11
+ */
12
+ export declare function getCurrentUser(): Promise<any>;
13
+ /**
14
+ * Limits the number of images in the content array, replacing excess images with text placeholders
15
+ * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
16
+ *
17
+ * @param content Array of content blocks that may contain images
18
+ * @param maxImages Maximum number of images to keep
19
+ * @returns Modified content array with limited images
20
+ */
21
+ export declare function limitImages(content: ContentBlock[], maxImages: number): ContentBlock[];
22
+ /**
23
+ * Function to get space details, using a cache to avoid redundant fetches
24
+ */
25
+ export declare function getSpaceDetails(spaceId: string): Promise<any>;
26
+ /**
27
+ * Get or create a task search index with specified filters
28
+ */
29
+ export declare function getTaskSearchIndex(space_ids?: string[], list_ids?: string[], assignees?: string[]): Promise<Fuse<any> | null>;
30
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAErC,OAAO,IAAI,MAAM,SAAS,CAAC;AAI3B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7C;AAKD;;;GAGG;AACH,wBAAsB,cAAc,iBAiBnC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CA8BtF;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B7D;AAKD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA4B3B"}