@taazkareem/clickup-mcp-server 0.6.8 → 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.
- package/README.md +1 -1
- package/build/mcp-tools.js +64 -0
- package/build/server-state.js +93 -0
- package/build/server.js +1 -1
- package/build/server.log +76 -0
- package/build/services/clickup/task/handlers.js +1 -0
- package/build/services/clickup/task.js +976 -0
- package/build/services/clickup/tools/tag.js +149 -0
- package/build/tools/bulk-tasks.js +36 -0
- package/build/tools/debug.js +76 -0
- package/build/tools/logs.js +55 -0
- package/build/tools/task.js +1554 -0
- package/build/utils/params-utils.js +39 -0
- package/build/utils/sponsor-analytics.js +100 -0
- package/build/utils/sponsor-utils.js +57 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|