@taazkareem/clickup-mcp-server 0.6.7 → 0.6.9

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Remove a tag from a task
3
+ * @param taskId Task ID or custom ID
4
+ * @param taskName Task name (for lookup if ID not provided)
5
+ * @param listName List name (optional, for improved task name lookup)
6
+ * @param tagName Tag name to remove
7
+ * @returns Success response
8
+ */
9
+ export async function removeTagFromTask(taskId, taskName, listName, tagName) {
10
+ logger.info(`[TAG REMOVAL] Starting tag removal for ${taskId || taskName}`);
11
+ try {
12
+ // Step 1: Find the task
13
+ let resolvedTaskId;
14
+ if (taskId) {
15
+ // If task ID is provided, use it directly
16
+ resolvedTaskId = taskId;
17
+ logger.info(`[TAG REMOVAL] Using provided task ID: ${resolvedTaskId}`);
18
+ }
19
+ else if (taskName) {
20
+ // If task name is provided, let's use a more direct approach
21
+ // First try to get tasks in the list if list name is provided
22
+ if (listName) {
23
+ logger.info(`[TAG REMOVAL] Looking up task by name in list "${listName}"`);
24
+ const list = await findListIDByName(clickUpServices.workspace, listName);
25
+ if (list) {
26
+ const tasks = await clickUpServices.task.getTasks(list.id);
27
+ // Try to find the task by name using a direct case-insensitive comparison
28
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
29
+ if (foundTask) {
30
+ resolvedTaskId = foundTask.id;
31
+ logger.info(`[TAG REMOVAL] Found task in list "${listName}": ${resolvedTaskId}`);
32
+ }
33
+ else {
34
+ // Try to find by substring
35
+ const substringMatch = tasks.find(t => t.name.toLowerCase().includes(taskName.toLowerCase()) ||
36
+ taskName.toLowerCase().includes(t.name.toLowerCase()));
37
+ if (substringMatch) {
38
+ resolvedTaskId = substringMatch.id;
39
+ logger.info(`[TAG REMOVAL] Found task in list "${listName}" by substring: ${resolvedTaskId}`);
40
+ }
41
+ else {
42
+ // If not found, try the alternative approach
43
+ logger.info(`[TAG REMOVAL] Task not found in list, trying global lookup`);
44
+ // Get all tasks in workspace
45
+ const filters = { list_ids: [list.id], include_closed: true };
46
+ const response = await clickUpServices.task.getWorkspaceTasks({ ...filters, detail_level: 'detailed' });
47
+ // Direct comparison of all tasks
48
+ if ('tasks' in response && response.tasks.length > 0) {
49
+ // Log first few tasks to see what's available
50
+ logger.info(`[TAG REMOVAL] Workspace search found ${response.tasks.length} tasks in list "${listName}"`);
51
+ // Try to find by exact name
52
+ const exactMatch = response.tasks.find(t => t.name === taskName);
53
+ if (exactMatch) {
54
+ resolvedTaskId = exactMatch.id;
55
+ logger.info(`[TAG REMOVAL] Found exact match: ${resolvedTaskId}`);
56
+ }
57
+ else {
58
+ // Try case-insensitive
59
+ const caseInsensitiveMatch = response.tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
60
+ if (caseInsensitiveMatch) {
61
+ resolvedTaskId = caseInsensitiveMatch.id;
62
+ logger.info(`[TAG REMOVAL] Found case-insensitive match: ${resolvedTaskId}`);
63
+ }
64
+ else {
65
+ throw new Error(`Task "${taskName}" not found in list "${listName}"`);
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ throw new Error(`No tasks found in list "${listName}"`);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ throw new Error(`List "${listName}" not found`);
77
+ }
78
+ }
79
+ else {
80
+ // If no list name, search across the workspace
81
+ logger.info(`[TAG REMOVAL] Searching for task by name across workspace: "${taskName}"`);
82
+ // Get all tasks across all lists instead of using global lookup
83
+ const response = await clickUpServices.task.getWorkspaceTasks({ include_closed: true, detail_level: 'detailed' });
84
+ if ('tasks' in response && response.tasks.length > 0) {
85
+ // Try exact match first
86
+ const exactMatch = response.tasks.find(t => t.name === taskName);
87
+ if (exactMatch) {
88
+ resolvedTaskId = exactMatch.id;
89
+ logger.info(`[TAG REMOVAL] Found exact match: ${resolvedTaskId}`);
90
+ }
91
+ else {
92
+ // Try case-insensitive
93
+ const caseInsensitiveMatch = response.tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
94
+ if (caseInsensitiveMatch) {
95
+ resolvedTaskId = caseInsensitiveMatch.id;
96
+ logger.info(`[TAG REMOVAL] Found case-insensitive match: ${resolvedTaskId}`);
97
+ }
98
+ else {
99
+ // Try substring match
100
+ const substringMatch = response.tasks.find(t => t.name.toLowerCase().includes(taskName.toLowerCase()) ||
101
+ taskName.toLowerCase().includes(t.name.toLowerCase()));
102
+ if (substringMatch) {
103
+ resolvedTaskId = substringMatch.id;
104
+ logger.info(`[TAG REMOVAL] Found substring match: ${resolvedTaskId}`);
105
+ }
106
+ else {
107
+ throw new Error(`Task "${taskName}" not found in workspace`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ else {
113
+ throw new Error(`No tasks found in workspace`);
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ throw new Error("Either taskId or taskName must be provided");
119
+ }
120
+ // Step 2: Get the current task to check for tags
121
+ const task = await clickUpServices.task.getTask(resolvedTaskId);
122
+ logger.info(`[TAG REMOVAL] Retrieved task ${resolvedTaskId}, current tags: ${task.tags.map(t => t.name).join(', ')}`);
123
+ // Step 3: Check if the tag exists on the task
124
+ const hasTag = task.tags.some(tag => tag.name.toLowerCase() === tagName.toLowerCase());
125
+ if (!hasTag) {
126
+ logger.info(`[TAG REMOVAL] Tag "${tagName}" not found on task ${resolvedTaskId}`);
127
+ return {
128
+ success: true,
129
+ message: "Tag removed from task successfully"
130
+ };
131
+ }
132
+ // Step 4: Remove the tag by updating the task
133
+ // Filter out the tag we want to remove
134
+ const updatedTags = task.tags.filter(tag => tag.name.toLowerCase() !== tagName.toLowerCase());
135
+ logger.info(`[TAG REMOVAL] Filtered tags from ${task.tags.length} to ${updatedTags.length}`);
136
+ // Use directly the ClickUp API to remove tag
137
+ const encodedTagName = encodeURIComponent(tagName);
138
+ await clickUpServices.task.client.delete(`/task/${resolvedTaskId}/tag/${encodedTagName}`);
139
+ logger.info(`[TAG REMOVAL] Successfully removed tag "${tagName}" from task ${resolvedTaskId}`);
140
+ return {
141
+ success: true,
142
+ message: "Tag removed from task successfully"
143
+ };
144
+ }
145
+ catch (error) {
146
+ logger.error(`[TAG REMOVAL] Error removing tag: ${error instanceof Error ? error.message : String(error)}`);
147
+ throw clickUpServices.task.handleError(error, `Failed to remove tag: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
149
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Alternative implementation for bulk tasks creation
3
+ */
4
+ import { handleCreateBulkTasks } from './task.js';
5
+ /**
6
+ * Alternative tool definition for bulk task creation to work around MCP validation issues
7
+ */
8
+ export const createTasksBulkTool = {
9
+ name: "create_tasks_bulk",
10
+ description: "Create multiple tasks in a list efficiently. You MUST provide:\n1. An array of tasks with required properties\n2. Either listId or listName to specify the target list",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ listId: {
15
+ type: "string",
16
+ description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
17
+ },
18
+ listName: {
19
+ type: "string",
20
+ description: "Name of list for new tasks. Only use if you don't have listId."
21
+ },
22
+ tasks: {
23
+ // Define minimally to avoid validation issues
24
+ description: "Array of tasks to create. Each task must have at least a name."
25
+ }
26
+ },
27
+ required: ["tasks"]
28
+ }
29
+ };
30
+ /**
31
+ * Handler for create_tasks_bulk tool
32
+ */
33
+ export async function handleCreateTasksBulk(parameters) {
34
+ // Use the same implementation as handleCreateBulkTasks
35
+ return handleCreateBulkTasks(parameters);
36
+ }
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Simple Logs Tool
3
+ *
4
+ * Provides a basic tool for reading server logs.
5
+ */
6
+ import { promises as fs } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ // Define the log file path - using __dirname to get absolute path
9
+ const LOG_FILE = join(dirname(__dirname), 'server.log');
10
+ // Tool definition
11
+ export const checkLogsTool = {
12
+ name: "check_logs",
13
+ description: "Check server logs with optional filtering by log level",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ level: {
18
+ type: "string",
19
+ enum: ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"],
20
+ description: "Filter logs by level"
21
+ },
22
+ limit: {
23
+ type: "number",
24
+ description: "Maximum number of log entries to return (default: 50)"
25
+ }
26
+ }
27
+ }
28
+ };
29
+ // Simple handler implementation using async/await with promises
30
+ export async function handleCheckLogs(params) {
31
+ try {
32
+ // Read log file using promises
33
+ const content = await fs.readFile(LOG_FILE, 'utf-8');
34
+ // Split into lines and filter out empty lines
35
+ let lines = content.split('\n').filter(Boolean);
36
+ // Apply level filtering if specified
37
+ if (params?.level) {
38
+ lines = lines.filter(line => line.includes(`${params.level}:`));
39
+ }
40
+ // Apply limit (default to 50)
41
+ const limit = params?.limit || 50;
42
+ const result = lines.slice(-limit);
43
+ // Return results
44
+ return {
45
+ logs: result.length > 0 ? result : ['No matching logs found.']
46
+ };
47
+ }
48
+ catch (error) {
49
+ // Simple error handling
50
+ if (error.code === 'ENOENT') {
51
+ return { logs: ['Log file not found.'] };
52
+ }
53
+ return { logs: [`Error reading logs: ${error.message}`] };
54
+ }
55
+ }
@@ -21,6 +21,8 @@ const { task: taskService, list: listService } = clickUpServices;
21
21
  const bulkService = new BulkService(taskService);
22
22
  // Create a logger instance for task handlers
23
23
  const logger = new Logger('TaskHandlers');
24
+ // Token limit constant for workspace tasks
25
+ const WORKSPACE_TASKS_TOKEN_LIMIT = 50000;
24
26
  // Cache for task context between sequential operations
25
27
  const taskContextCache = new Map();
26
28
  const TASK_CONTEXT_TTL = 5 * 60 * 1000; // 5 minutes
@@ -446,6 +448,39 @@ export async function createTaskCommentHandler(params) {
446
448
  throw error;
447
449
  }
448
450
  }
451
+ /**
452
+ * Estimate tokens for a task response
453
+ * This is a simplified estimation - adjust based on actual token counting needs
454
+ */
455
+ function estimateTaskResponseTokens(task) {
456
+ // Base estimation for task structure
457
+ let tokenCount = 0;
458
+ // Core fields
459
+ tokenCount += (task.name?.length || 0) / 4; // Approximate tokens for name
460
+ tokenCount += (task.description?.length || 0) / 4; // Approximate tokens for description
461
+ tokenCount += (task.text_content?.length || 0) / 4; // Use text_content instead of markdown_description
462
+ // Status and other metadata
463
+ tokenCount += 5; // Basic metadata fields
464
+ // Custom fields
465
+ if (task.custom_fields) {
466
+ tokenCount += Object.keys(task.custom_fields).length * 10; // Rough estimate per custom field
467
+ }
468
+ // Add overhead for JSON structure
469
+ tokenCount *= 1.1;
470
+ return Math.ceil(tokenCount);
471
+ }
472
+ /**
473
+ * Check if response would exceed token limit
474
+ */
475
+ function wouldExceedTokenLimit(response) {
476
+ if (!response.tasks?.length)
477
+ return false;
478
+ // Calculate total estimated tokens
479
+ const totalTokens = response.tasks.reduce((sum, task) => sum + estimateTaskResponseTokens(task), 0);
480
+ // Add overhead for response structure
481
+ const estimatedTotal = totalTokens * 1.1;
482
+ return estimatedTotal > WORKSPACE_TASKS_TOKEN_LIMIT;
483
+ }
449
484
  /**
450
485
  * Handler for getting workspace tasks with filtering
451
486
  */
@@ -495,6 +530,16 @@ export async function getWorkspaceTasksHandler(taskService, params) {
495
530
  };
496
531
  // Get tasks with adaptive response format support
497
532
  const response = await taskService.getWorkspaceTasks(filters);
533
+ // Check token limit at handler level
534
+ if (params.detail_level !== 'summary' && wouldExceedTokenLimit(response)) {
535
+ logger.info('Response would exceed token limit, fetching summary format instead');
536
+ // Refetch with summary format
537
+ const summaryResponse = await taskService.getWorkspaceTasks({
538
+ ...filters,
539
+ detail_level: 'summary'
540
+ });
541
+ return summaryResponse;
542
+ }
498
543
  // Return the response without adding the redundant _note field
499
544
  return response;
500
545
  }