@taazkareem/clickup-mcp-server 0.4.68 → 0.4.70

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.
@@ -158,7 +158,7 @@ export class TaskService extends BaseClickUpService {
158
158
  }
159
159
  }
160
160
  /**
161
- * Find a task by its name in a specific list
161
+ * Find a task by name within a list
162
162
  * @param listId The list ID to search within
163
163
  * @param taskName The name of the task to find
164
164
  * @returns The task if found, otherwise null
@@ -167,7 +167,26 @@ export class TaskService extends BaseClickUpService {
167
167
  this.logOperation('findTaskByName', { listId, taskName });
168
168
  try {
169
169
  const tasks = await this.getTasks(listId);
170
- const matchingTask = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
170
+ // Normalize the search term
171
+ const normalizedSearchTerm = taskName.toLowerCase().trim();
172
+ // First try exact match
173
+ let matchingTask = tasks.find(task => task.name.toLowerCase().trim() === normalizedSearchTerm);
174
+ // If no exact match, try substring match
175
+ if (!matchingTask) {
176
+ matchingTask = tasks.find(task => task.name.toLowerCase().trim().includes(normalizedSearchTerm) ||
177
+ normalizedSearchTerm.includes(task.name.toLowerCase().trim()));
178
+ }
179
+ // If still no match and there are emoji characters, try matching without emoji
180
+ if (!matchingTask && /[\p{Emoji}]/u.test(normalizedSearchTerm)) {
181
+ // Remove emoji and try again (simple approximation)
182
+ const withoutEmoji = normalizedSearchTerm.replace(/[\p{Emoji}]/gu, '').trim();
183
+ matchingTask = tasks.find(task => {
184
+ const taskNameWithoutEmoji = task.name.toLowerCase().replace(/[\p{Emoji}]/gu, '').trim();
185
+ return taskNameWithoutEmoji === withoutEmoji ||
186
+ taskNameWithoutEmoji.includes(withoutEmoji) ||
187
+ withoutEmoji.includes(taskNameWithoutEmoji);
188
+ });
189
+ }
171
190
  return matchingTask || null;
172
191
  }
173
192
  catch (error) {
@@ -489,4 +508,29 @@ export class TaskService extends BaseClickUpService {
489
508
  throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
490
509
  }
491
510
  }
511
+ /**
512
+ * Get comments for a specific task
513
+ * @param taskId The ID of the task to retrieve comments for
514
+ * @param start Optional parameter for pagination, timestamp from which to start fetching comments
515
+ * @param startId Optional parameter for pagination, comment ID from which to start fetching
516
+ * @returns Array of task comments
517
+ */
518
+ async getTaskComments(taskId, start, startId) {
519
+ this.logOperation('getTaskComments', { taskId, start, startId });
520
+ try {
521
+ return await this.makeRequest(async () => {
522
+ const params = new URLSearchParams();
523
+ // Add pagination parameters if provided
524
+ if (start)
525
+ params.append('start', String(start));
526
+ if (startId)
527
+ params.append('start_id', startId);
528
+ const response = await this.client.get(`/task/${taskId}/comment`, { params });
529
+ return response.data.comments;
530
+ });
531
+ }
532
+ catch (error) {
533
+ throw this.handleError(error, `Failed to retrieve comments for task ${taskId}`);
534
+ }
535
+ }
492
536
  }
@@ -145,7 +145,9 @@ export class WorkspaceService extends BaseClickUpService {
145
145
  }
146
146
  // Get lists directly in the space (not in any folder)
147
147
  const listsInSpace = await this.getListsInSpace(space.id);
148
+ this.logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name} (${space.id})`);
148
149
  for (const list of listsInSpace) {
150
+ this.logger.debug(`Adding list directly to space: ${list.name} (${list.id})`);
149
151
  spaceNode.children?.push({
150
152
  id: list.id,
151
153
  name: list.name,
@@ -218,11 +220,11 @@ export class WorkspaceService extends BaseClickUpService {
218
220
  return space ? space.id : null;
219
221
  }
220
222
  /**
221
- * Get lists from the API (using the appropriate endpoint based on context)
223
+ * Get folderless lists from the API (lists that are directly in a space)
222
224
  * @param spaceId - The ID of the space
223
225
  * @returns - Promise resolving to array of lists
224
226
  */
225
- async getLists(spaceId) {
227
+ async getFolderlessLists(spaceId) {
226
228
  try {
227
229
  const response = await this.makeRequest(async () => {
228
230
  const result = await this.client.get(`/space/${spaceId}/list`);
@@ -231,7 +233,24 @@ export class WorkspaceService extends BaseClickUpService {
231
233
  return response.lists || [];
232
234
  }
233
235
  catch (error) {
234
- throw this.handleError(error, `Failed to get lists for space ${spaceId}`);
236
+ throw this.handleError(error, `Failed to get folderless lists for space ${spaceId}`);
237
+ }
238
+ }
239
+ /**
240
+ * Get lists in a space (not in any folder)
241
+ * @param spaceId - The ID of the space
242
+ * @returns - Promise resolving to array of lists
243
+ */
244
+ async getListsInSpace(spaceId) {
245
+ try {
246
+ // The /space/{space_id}/list endpoint already returns folderless lists only
247
+ const lists = await this.getFolderlessLists(spaceId);
248
+ this.logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
249
+ // Return all lists without filtering since the API already returns folderless lists
250
+ return lists;
251
+ }
252
+ catch (error) {
253
+ throw this.handleError(error, `Failed to get lists in space ${spaceId}`);
235
254
  }
236
255
  }
237
256
  /**
@@ -267,20 +286,6 @@ export class WorkspaceService extends BaseClickUpService {
267
286
  throw this.handleError(error, `Failed to get folder with ID ${folderId}`);
268
287
  }
269
288
  }
270
- /**
271
- * Get lists in a space (not in any folder)
272
- * @param spaceId - The ID of the space
273
- * @returns - Promise resolving to array of lists
274
- */
275
- async getListsInSpace(spaceId) {
276
- try {
277
- const lists = await this.getLists(spaceId);
278
- return lists.filter((list) => !list.folder); // Filter lists not in a folder
279
- }
280
- catch (error) {
281
- throw this.handleError(error, `Failed to get lists in space ${spaceId}`);
282
- }
283
- }
284
289
  /**
285
290
  * Get folders in a space
286
291
  * @param spaceId - The ID of the space
@@ -6,10 +6,30 @@
6
6
  */
7
7
  import { createClickUpServices } from './clickup/index.js';
8
8
  import config from '../config.js';
9
+ import { Logger } from '../logger.js';
10
+ const logger = new Logger('SharedServices');
11
+ // Singleton instance
12
+ let servicesInstance = null;
13
+ /**
14
+ * Get or create the ClickUp services instance
15
+ */
16
+ function getClickUpServices() {
17
+ if (!servicesInstance) {
18
+ logger.info('Creating shared ClickUp services singleton');
19
+ // Create the services instance
20
+ servicesInstance = createClickUpServices({
21
+ apiKey: config.clickupApiKey,
22
+ teamId: config.clickupTeamId
23
+ });
24
+ // Log what services were initialized with more clarity
25
+ logger.info('Services initialization complete', {
26
+ services: Object.keys(servicesInstance).join(', '),
27
+ teamId: config.clickupTeamId
28
+ });
29
+ }
30
+ return servicesInstance;
31
+ }
9
32
  // Create a single instance of ClickUp services to be shared
10
- export const clickUpServices = createClickUpServices({
11
- apiKey: config.clickupApiKey,
12
- teamId: config.clickupTeamId
13
- });
33
+ export const clickUpServices = getClickUpServices();
14
34
  // Export individual services for convenience
15
35
  export const { list: listService, task: taskService, folder: folderService, workspace: workspaceService } = clickUpServices;
@@ -0,0 +1,452 @@
1
+ /**
2
+ * ClickUp MCP Cache Module
3
+ *
4
+ * This module provides caching for workspace data including:
5
+ * - Workspace hierarchy (spaces, folders, lists)
6
+ * - Task data (names, IDs, and metadata)
7
+ *
8
+ * The cache is initialized on server startup and updated when
9
+ * tasks, lists, folders, or spaces are modified.
10
+ */
11
+ import { createClickUpServices } from '../services/clickup/index.js';
12
+ import config from '../config.js';
13
+ import { log } from '../logger.js';
14
+ // Initialize ClickUp services
15
+ const services = createClickUpServices({
16
+ apiKey: config.clickupApiKey,
17
+ teamId: config.clickupTeamId
18
+ });
19
+ const { workspace: workspaceService, task: taskService, list: listService, folder: folderService } = services;
20
+ /**
21
+ * Singleton cache instance
22
+ */
23
+ let workspaceCache = null;
24
+ /**
25
+ * Normalize a name for case-insensitive lookups
26
+ */
27
+ function normalizeName(name) {
28
+ return name.toLowerCase().trim();
29
+ }
30
+ /**
31
+ * Generate a unique key for task name lookup
32
+ */
33
+ function getTaskNameKey(listId, taskName) {
34
+ return `${listId}:${normalizeName(taskName)}`;
35
+ }
36
+ /**
37
+ * Initialize an empty cache structure
38
+ */
39
+ function createEmptyCache() {
40
+ return {
41
+ hierarchy: {
42
+ root: {
43
+ id: config.clickupTeamId,
44
+ name: 'Workspace',
45
+ children: []
46
+ }
47
+ },
48
+ tasksById: new Map(),
49
+ tasksByName: new Map(),
50
+ listIdsByName: new Map(),
51
+ folderIdsByName: new Map(),
52
+ spaceIdsByName: new Map(),
53
+ lastUpdated: new Date(),
54
+ initialized: false
55
+ };
56
+ }
57
+ /**
58
+ * Process workspace hierarchy to populate ID lookup maps
59
+ */
60
+ function processHierarchy(hierarchy) {
61
+ if (!workspaceCache) {
62
+ workspaceCache = createEmptyCache();
63
+ }
64
+ // Clear existing maps
65
+ workspaceCache.listIdsByName.clear();
66
+ workspaceCache.folderIdsByName.clear();
67
+ workspaceCache.spaceIdsByName.clear();
68
+ // Recursively process all nodes
69
+ const processNode = (node) => {
70
+ const normalizedName = normalizeName(node.name);
71
+ // Store in appropriate map based on node type
72
+ switch (node.type) {
73
+ case 'space':
74
+ workspaceCache.spaceIdsByName.set(normalizedName, node.id);
75
+ break;
76
+ case 'folder':
77
+ workspaceCache.folderIdsByName.set(normalizedName, node.id);
78
+ break;
79
+ case 'list':
80
+ workspaceCache.listIdsByName.set(normalizedName, node.id);
81
+ break;
82
+ }
83
+ // Process children recursively
84
+ if (node.children && node.children.length > 0) {
85
+ node.children.forEach(child => processNode(child));
86
+ }
87
+ };
88
+ // Process all space nodes (children of the root)
89
+ hierarchy.root.children.forEach(node => processNode(node));
90
+ }
91
+ /**
92
+ * Cache a single task
93
+ */
94
+ function cacheTask(task) {
95
+ if (!workspaceCache) {
96
+ workspaceCache = createEmptyCache();
97
+ }
98
+ // Create cached task object
99
+ const cachedTask = {
100
+ id: task.id,
101
+ name: task.name,
102
+ listId: task.list.id,
103
+ status: task.status?.status,
104
+ dueDate: task.due_date,
105
+ priority: task.priority?.priority ? Number(task.priority.priority) : null,
106
+ createdDate: task.date_created,
107
+ updatedDate: task.date_updated
108
+ };
109
+ // Store in maps
110
+ workspaceCache.tasksById.set(task.id, cachedTask);
111
+ workspaceCache.tasksByName.set(getTaskNameKey(task.list.id, task.name), task.id);
112
+ }
113
+ /**
114
+ * Remove a task from the cache
115
+ */
116
+ function removeCachedTask(taskId) {
117
+ if (!workspaceCache || !workspaceCache.tasksById.has(taskId)) {
118
+ return;
119
+ }
120
+ // Get task details before removing
121
+ const task = workspaceCache.tasksById.get(taskId);
122
+ if (task) {
123
+ // Remove from name lookup
124
+ const nameKey = getTaskNameKey(task.listId, task.name);
125
+ workspaceCache.tasksByName.delete(nameKey);
126
+ // Remove from ID lookup
127
+ workspaceCache.tasksById.delete(taskId);
128
+ }
129
+ }
130
+ /**
131
+ * Update a cached task with new data
132
+ */
133
+ function updateCachedTask(taskId, updates) {
134
+ if (!workspaceCache || !workspaceCache.tasksById.has(taskId)) {
135
+ return;
136
+ }
137
+ const task = workspaceCache.tasksById.get(taskId);
138
+ if (!task)
139
+ return;
140
+ // Handle name change - requires updating the tasksByName map
141
+ if (updates.name && updates.name !== task.name) {
142
+ // Remove old name lookup
143
+ const oldNameKey = getTaskNameKey(task.listId, task.name);
144
+ workspaceCache.tasksByName.delete(oldNameKey);
145
+ // Add new name lookup
146
+ const newNameKey = getTaskNameKey(task.listId, updates.name);
147
+ workspaceCache.tasksByName.set(newNameKey, taskId);
148
+ }
149
+ // Handle list change - requires updating the tasksByName map
150
+ if (updates.listId && updates.listId !== task.listId) {
151
+ // Remove old list-based lookup
152
+ const oldNameKey = getTaskNameKey(task.listId, task.name);
153
+ workspaceCache.tasksByName.delete(oldNameKey);
154
+ // Add new list-based lookup
155
+ const newNameKey = getTaskNameKey(updates.listId, task.name);
156
+ workspaceCache.tasksByName.set(newNameKey, taskId);
157
+ }
158
+ // Update the task object with all provided updates
159
+ Object.assign(task, updates);
160
+ // Update the task in the cache
161
+ workspaceCache.tasksById.set(taskId, task);
162
+ // Update last updated timestamp
163
+ workspaceCache.lastUpdated = new Date();
164
+ }
165
+ /**
166
+ * Cache all tasks for a list
167
+ */
168
+ async function cacheTasksForList(listId) {
169
+ try {
170
+ log('debug', `Fetching tasks for list ${listId}...`);
171
+ const tasks = await taskService.getTasks(listId);
172
+ log('debug', `Caching ${tasks.length} tasks for list ${listId}`);
173
+ tasks.forEach(task => {
174
+ cacheTask(task);
175
+ });
176
+ // Update last updated timestamp
177
+ if (workspaceCache) {
178
+ workspaceCache.lastUpdated = new Date();
179
+ }
180
+ }
181
+ catch (error) {
182
+ log('error', `Error caching tasks for list ${listId}:`, error);
183
+ }
184
+ }
185
+ /**
186
+ * Initialize the workspace cache
187
+ * This should be called on server startup
188
+ */
189
+ export async function initializeCache() {
190
+ try {
191
+ log('info', 'Initializing workspace cache...');
192
+ // Create new cache
193
+ workspaceCache = createEmptyCache();
194
+ log('debug', 'Empty cache structure created');
195
+ // Get workspace hierarchy
196
+ log('debug', 'Fetching workspace hierarchy from ClickUp API...');
197
+ const hierarchy = await workspaceService.getWorkspaceHierarchy(true);
198
+ workspaceCache.hierarchy = hierarchy;
199
+ // Count spaces, folders, and lists in the hierarchy
200
+ const stats = countHierarchyItems(hierarchy);
201
+ log('info', 'Workspace hierarchy fetched successfully', stats);
202
+ // Process hierarchy to populate ID lookup maps
203
+ log('debug', 'Processing hierarchy to build lookup maps...');
204
+ processHierarchy(hierarchy);
205
+ // Log the number of items in each lookup map
206
+ log('info', 'Lookup maps populated', {
207
+ spaces: workspaceCache.spaceIdsByName.size,
208
+ folders: workspaceCache.folderIdsByName.size,
209
+ lists: workspaceCache.listIdsByName.size
210
+ });
211
+ // Cache tasks for each list
212
+ log('info', 'Beginning task caching process for all lists...');
213
+ // Function to find all list IDs in the hierarchy
214
+ const getAllListIds = (node) => {
215
+ let listIds = [];
216
+ if (node.type === 'list') {
217
+ listIds.push(node.id);
218
+ }
219
+ if (node.children && node.children.length > 0) {
220
+ node.children.forEach(child => {
221
+ listIds = [...listIds, ...getAllListIds(child)];
222
+ });
223
+ }
224
+ return listIds;
225
+ };
226
+ // Get all list IDs from all spaces
227
+ const listIds = [];
228
+ hierarchy.root.children.forEach(spaceNode => {
229
+ listIds.push(...getAllListIds(spaceNode));
230
+ });
231
+ log('info', `Found ${listIds.length} lists to cache tasks from`);
232
+ // Cache tasks for each list (sequentially to avoid rate limiting)
233
+ let completedLists = 0;
234
+ let totalTasksCached = 0;
235
+ for (const listId of listIds) {
236
+ // Get list name if available
237
+ const listName = getListNameById(listId, hierarchy);
238
+ log('debug', `Processing list ${completedLists + 1}/${listIds.length}: ${listName || listId}`);
239
+ // Get task count before caching
240
+ const taskCountBefore = workspaceCache.tasksById.size;
241
+ await cacheTasksForList(listId);
242
+ // Calculate how many tasks were added
243
+ const taskCountAfter = workspaceCache.tasksById.size;
244
+ const tasksAdded = taskCountAfter - taskCountBefore;
245
+ totalTasksCached += tasksAdded;
246
+ completedLists++;
247
+ // Log progress every 5 lists or for the last list
248
+ if (completedLists % 5 === 0 || completedLists === listIds.length) {
249
+ log('info', `Cache progress: ${completedLists}/${listIds.length} lists processed (${Math.round(completedLists / listIds.length * 100)}%)`, {
250
+ totalTasksCached,
251
+ completedLists,
252
+ remainingLists: listIds.length - completedLists
253
+ });
254
+ }
255
+ // Small delay to avoid rate limiting
256
+ await new Promise(resolve => setTimeout(resolve, 100));
257
+ }
258
+ // Mark cache as initialized
259
+ workspaceCache.initialized = true;
260
+ workspaceCache.lastUpdated = new Date();
261
+ // Log final cache stats
262
+ const cacheStats = getCacheStats();
263
+ log('info', `Workspace cache initialized successfully`, cacheStats);
264
+ }
265
+ catch (error) {
266
+ log('error', 'Error initializing workspace cache:', error);
267
+ // Reset cache on error
268
+ workspaceCache = null;
269
+ // Re-throw error
270
+ throw error;
271
+ }
272
+ }
273
+ /**
274
+ * Helper function to count items in the hierarchy
275
+ */
276
+ function countHierarchyItems(hierarchy) {
277
+ let spaces = 0;
278
+ let folders = 0;
279
+ let lists = 0;
280
+ const countNode = (node) => {
281
+ switch (node.type) {
282
+ case 'space':
283
+ spaces++;
284
+ break;
285
+ case 'folder':
286
+ folders++;
287
+ break;
288
+ case 'list':
289
+ lists++;
290
+ break;
291
+ }
292
+ if (node.children && node.children.length > 0) {
293
+ node.children.forEach(countNode);
294
+ }
295
+ };
296
+ // Count all spaces (children of root)
297
+ hierarchy.root.children.forEach(countNode);
298
+ return { spaces, folders, lists };
299
+ }
300
+ /**
301
+ * Helper function to get list name by ID
302
+ */
303
+ function getListNameById(listId, hierarchy) {
304
+ let result = null;
305
+ const findListName = (node) => {
306
+ if (node.type === 'list' && node.id === listId) {
307
+ result = node.name;
308
+ return true;
309
+ }
310
+ if (node.children) {
311
+ for (const child of node.children) {
312
+ if (findListName(child)) {
313
+ return true;
314
+ }
315
+ }
316
+ }
317
+ return false;
318
+ };
319
+ hierarchy.root.children.forEach(findListName);
320
+ return result;
321
+ }
322
+ /**
323
+ * Check if the cache is initialized
324
+ */
325
+ export function isCacheInitialized() {
326
+ return !!(workspaceCache && workspaceCache.initialized);
327
+ }
328
+ /**
329
+ * Get the workspace hierarchy from cache
330
+ */
331
+ export function getCachedHierarchy() {
332
+ return workspaceCache?.hierarchy || null;
333
+ }
334
+ /**
335
+ * Find an entity ID by name from the cache
336
+ */
337
+ export function findIDByNameInCache(name, type) {
338
+ if (!workspaceCache || !workspaceCache.initialized) {
339
+ return null;
340
+ }
341
+ const normalizedName = normalizeName(name);
342
+ switch (type) {
343
+ case 'space':
344
+ return workspaceCache.spaceIdsByName.get(normalizedName) || null;
345
+ case 'folder':
346
+ return workspaceCache.folderIdsByName.get(normalizedName) || null;
347
+ case 'list':
348
+ return workspaceCache.listIdsByName.get(normalizedName) || null;
349
+ default:
350
+ return null;
351
+ }
352
+ }
353
+ /**
354
+ * Find a task ID by name and list ID from the cache
355
+ */
356
+ export function findTaskIDByName(taskName, listId) {
357
+ if (!workspaceCache || !workspaceCache.initialized) {
358
+ return null;
359
+ }
360
+ const nameKey = getTaskNameKey(listId, taskName);
361
+ return workspaceCache.tasksByName.get(nameKey) || null;
362
+ }
363
+ /**
364
+ * Get a task by ID from the cache
365
+ */
366
+ export function getTaskFromCache(taskId) {
367
+ if (!workspaceCache || !workspaceCache.initialized) {
368
+ return null;
369
+ }
370
+ return workspaceCache.tasksById.get(taskId) || null;
371
+ }
372
+ /**
373
+ * Add a new task to the cache
374
+ */
375
+ export function addTaskToCache(task) {
376
+ if (!workspaceCache || !workspaceCache.initialized) {
377
+ return;
378
+ }
379
+ cacheTask(task);
380
+ workspaceCache.lastUpdated = new Date();
381
+ }
382
+ /**
383
+ * Remove a task from the cache
384
+ */
385
+ export function removeTaskFromCache(taskId) {
386
+ if (!workspaceCache || !workspaceCache.initialized) {
387
+ return;
388
+ }
389
+ removeCachedTask(taskId);
390
+ workspaceCache.lastUpdated = new Date();
391
+ }
392
+ /**
393
+ * Update a task in the cache
394
+ */
395
+ export function updateTaskInCache(taskId, updates) {
396
+ if (!workspaceCache || !workspaceCache.initialized) {
397
+ return;
398
+ }
399
+ updateCachedTask(taskId, updates);
400
+ }
401
+ /**
402
+ * Refresh the tasks for a specific list in the cache
403
+ */
404
+ export async function refreshListTasksInCache(listId) {
405
+ if (!workspaceCache || !workspaceCache.initialized) {
406
+ return;
407
+ }
408
+ // Remove existing tasks for this list
409
+ const tasksToRemove = [];
410
+ workspaceCache.tasksById.forEach((task, id) => {
411
+ if (task.listId === listId) {
412
+ tasksToRemove.push(id);
413
+ }
414
+ });
415
+ tasksToRemove.forEach(taskId => {
416
+ removeCachedTask(taskId);
417
+ });
418
+ // Cache tasks for the list
419
+ await cacheTasksForList(listId);
420
+ }
421
+ /**
422
+ * Refresh the entire workspace cache
423
+ */
424
+ export async function refreshCache() {
425
+ // Reset cache
426
+ workspaceCache = null;
427
+ // Re-initialize
428
+ await initializeCache();
429
+ }
430
+ /**
431
+ * Get cache statistics for monitoring
432
+ */
433
+ export function getCacheStats() {
434
+ if (!workspaceCache) {
435
+ return {
436
+ initialized: false,
437
+ tasksCount: 0,
438
+ listsCount: 0,
439
+ foldersCount: 0,
440
+ spacesCount: 0,
441
+ lastUpdated: null
442
+ };
443
+ }
444
+ return {
445
+ initialized: workspaceCache.initialized,
446
+ tasksCount: workspaceCache.tasksById.size,
447
+ listsCount: workspaceCache.listIdsByName.size,
448
+ foldersCount: workspaceCache.folderIdsByName.size,
449
+ spacesCount: workspaceCache.spaceIdsByName.size,
450
+ lastUpdated: workspaceCache.lastUpdated
451
+ };
452
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Debug Tools
3
+ *
4
+ * This module provides tools for debugging and monitoring the ClickUp MCP server.
5
+ */
6
+ // In-memory log storage
7
+ const DEBUG_LOGS = [];
8
+ /**
9
+ * Log a message to the in-memory debug logs
10
+ */
11
+ export function logDebug(message, level = 'info') {
12
+ const timestamp = new Date().toISOString();
13
+ DEBUG_LOGS.push({ timestamp, message, level });
14
+ // Optional: Also print to console for development
15
+ if (level === 'error') {
16
+ console.error(`[${timestamp}] ERROR: ${message}`);
17
+ }
18
+ else if (level === 'warn') {
19
+ console.warn(`[${timestamp}] WARN: ${message}`);
20
+ }
21
+ else {
22
+ console.log(`[${timestamp}] INFO: ${message}`);
23
+ }
24
+ }
25
+ /**
26
+ * Tool definition for checking debug logs
27
+ */
28
+ export const checkDebugLogsTool = {
29
+ name: 'check_debug_logs',
30
+ description: 'Check the server debug logs collected since the server started.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {}
34
+ }
35
+ };
36
+ /**
37
+ * Tool definition for clearing debug logs
38
+ */
39
+ export const clearDebugLogsTool = {
40
+ name: 'clear_debug_logs',
41
+ description: 'Clear all server debug logs.',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {}
45
+ }
46
+ };
47
+ /**
48
+ * Handler for the check_debug_logs tool
49
+ */
50
+ export function handleCheckDebugLogs() {
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: DEBUG_LOGS.length > 0
56
+ ? DEBUG_LOGS.map(log => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`).join('\n')
57
+ : "No debug logs available."
58
+ }
59
+ ]
60
+ };
61
+ }
62
+ /**
63
+ * Handler for the clear_debug_logs tool
64
+ */
65
+ export function handleClearDebugLogs() {
66
+ const count = DEBUG_LOGS.length;
67
+ DEBUG_LOGS.length = 0;
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Cleared ${count} debug log entries.`
73
+ }
74
+ ]
75
+ };
76
+ }