@taazkareem/clickup-mcp-server 0.8.1 → 0.8.3

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI applications. This server allows AI agents to interact with ClickUp tasks, spaces, lists, and folders through a standardized protocol.
8
8
 
9
- > 🚀 **Status Update:** v0.8.0 is now available with HTTP Streamable transport support, Legacy SSE Support, Member Management tools, and more.
9
+ > 🚀 **Status Update:** v0.8.3 is released with major enhancements! Enhanced workspace tasks filtering with Views API support for multi-list tasks (Issue #43), added ENABLED_TOOLS configuration option (Issue #50), and fixed automatic priority assignment in task creation. See [Release Notes](release-notes.md) for full details.
10
10
 
11
11
  ## Setup
12
12
 
@@ -55,9 +55,45 @@ Or use this npx command:
55
55
 
56
56
  **Obs: if you don't pass "DOCUMENT_SUPPORT": "true", the default is false and document support will not be active.**
57
57
 
58
- Additionally, you can use the `DISABLED_TOOLS` environment variable or `--env DISABLED_TOOLS` argument to disable specific tools. Provide a comma-separated list of tool names to disable (e.g., `create_task,delete_task`).
58
+ ### Tool Filtering
59
59
 
60
- Please disable tools you don't need if you are having issues with the number of tools or any context limitations
60
+ You can control which tools are available using two complementary environment variables:
61
+
62
+ #### ENABLED_TOOLS (Recommended)
63
+ Use `ENABLED_TOOLS` to specify exactly which tools should be available:
64
+ ```bash
65
+ # Environment variable
66
+ export ENABLED_TOOLS="create_task,get_task,update_task,get_workspace_hierarchy"
67
+
68
+ # Command line argument
69
+ --env ENABLED_TOOLS=create_task,get_task,update_task,get_workspace_hierarchy
70
+ ```
71
+
72
+ #### DISABLED_TOOLS (Legacy)
73
+ Use `DISABLED_TOOLS` to disable specific tools while keeping all others enabled:
74
+ ```bash
75
+ # Environment variable
76
+ export DISABLED_TOOLS="delete_task,delete_bulk_tasks"
77
+
78
+ # Command line argument
79
+ --env DISABLED_TOOLS=delete_task,delete_bulk_tasks
80
+ ```
81
+
82
+ #### Precedence Rules
83
+ - If `ENABLED_TOOLS` is specified, only those tools will be available (takes precedence over `DISABLED_TOOLS`)
84
+ - If only `DISABLED_TOOLS` is specified, all tools except those listed will be available
85
+ - If neither is specified, all tools are available (default behavior)
86
+
87
+ **Example:**
88
+ ```bash
89
+ # Only enable task creation and reading tools
90
+ npx -y @taazkareem/clickup-mcp-server@latest \
91
+ --env CLICKUP_API_KEY=your-api-key \
92
+ --env CLICKUP_TEAM_ID=your-team-id \
93
+ --env ENABLED_TOOLS=create_task,get_task,get_workspace_hierarchy
94
+ ```
95
+
96
+ Please filter tools you don't need if you are having issues with the number of tools or any context limitations.
61
97
 
62
98
  ## Running with HTTP Transport Support
63
99
 
@@ -97,6 +133,8 @@ Available configuration options:
97
133
 
98
134
  | Option | Description | Default |
99
135
  | ------ | ----------- | ------- |
136
+ | `ENABLED_TOOLS` | Comma-separated list of tools to enable (takes precedence) | All tools |
137
+ | `DISABLED_TOOLS` | Comma-separated list of tools to disable | None |
100
138
  | `ENABLE_SSE` | Enable the HTTP/SSE transport | `false` |
101
139
  | `PORT` | Port for the HTTP server | `3231` |
102
140
  | `ENABLE_STDIO` | Enable the STDIO transport | `true` |
@@ -142,55 +180,55 @@ npm run sse-client
142
180
 
143
181
  | Tool | Description | Required Parameters |
144
182
  | ------------------------------------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
145
- | [get_workspace_hierarchy](docs/api-reference.md#workspace-navigation) | Get workspace structure | None |
146
- | [create_task](docs/api-reference.md#task-management) | Create a task | `name`, (`listId`/`listName`) |
147
- | [create_bulk_tasks](docs/api-reference.md#task-management) | Create multiple tasks | `tasks[]` |
148
- | [update_task](docs/api-reference.md#task-management) | Modify task | `taskId`/`taskName` |
149
- | [update_bulk_tasks](docs/api-reference.md#task-management) | Update multiple tasks | `tasks[]` with IDs or names |
150
- | [get_tasks](docs/api-reference.md#task-management) | Get tasks from list | `listId`/`listName` |
151
- | [get_task](docs/api-reference.md#task-management) | Get single task details | `taskId`/`taskName` (with smart disambiguation) |
152
- | [get_workspace_tasks](docs/api-reference.md#task-management) | Get tasks with filtering | At least one filter (tags, list_ids, space_ids, etc.) |
153
- | [get_task_comments](docs/api-reference.md#task-management) | Get comments on a task | `taskId`/`taskName` |
154
- | [create_task_comment](docs/api-reference.md#task-management) | Add a comment to a task | `commentText`, (`taskId`/(`taskName`+`listName`)) |
155
- | [attach_task_file](docs/api-reference.md#task-management) | Attach file to a task | `taskId`/`taskName`, (`file_data` or `file_url`) |
156
- | [delete_task](docs/api-reference.md#task-management) | Remove task | `taskId`/`taskName` |
157
- | [delete_bulk_tasks](docs/api-reference.md#task-management) | Remove multiple tasks | `tasks[]` with IDs or names |
158
- | [move_task](docs/api-reference.md#task-management) | Move task | `taskId`/`taskName`, `listId`/`listName` |
159
- | [move_bulk_tasks](docs/api-reference.md#task-management) | Move multiple tasks | `tasks[]` with IDs or names, target list |
160
- | [duplicate_task](docs/api-reference.md#task-management) | Copy task | `taskId`/`taskName`, `listId`/`listName` |
161
- | [create_list](docs/api-reference.md#list-management) | Create list in space | `name`, `spaceId`/`spaceName` |
162
- | [create_folder](docs/api-reference.md#folder-management) | Create folder | `name`, `spaceId`/`spaceName` |
163
- | [create_list_in_folder](docs/api-reference.md#list-management) | Create list in folder | `name`, `folderId`/`folderName` |
164
- | [get_folder](docs/api-reference.md#folder-management) | Get folder details | `folderId`/`folderName` |
165
- | [update_folder](docs/api-reference.md#folder-management) | Update folder properties | `folderId`/`folderName` |
166
- | [delete_folder](docs/api-reference.md#folder-management) | Delete folder | `folderId`/`folderName` |
167
- | [get_list](docs/api-reference.md#list-management) | Get list details | `listId`/`listName` |
168
- | [update_list](docs/api-reference.md#list-management) | Update list properties | `listId`/`listName` |
169
- | [delete_list](docs/api-reference.md#list-management) | Delete list | `listId`/`listName` |
170
- | [get_space_tags](docs/api-reference.md#tag-management) | Get space tags | `spaceId`/`spaceName` |
171
- | [create_space_tag](docs/api-reference.md#tag-management) | Create tag | `tagName`, `spaceId`/`spaceName` |
172
- | [update_space_tag](docs/api-reference.md#tag-management) | Update tag | `tagName`, `spaceId`/`spaceName` |
173
- | [delete_space_tag](docs/api-reference.md#tag-management) | Delete tag | `tagName`, `spaceId`/`spaceName` |
174
- | [add_tag_to_task](docs/api-reference.md#tag-management) | Add tag to task | `tagName`, `taskId`/(`taskName`+`listName`) |
175
- | [remove_tag_from_task](docs/api-reference.md#tag-management) | Remove tag from task | `tagName`, `taskId`/(`taskName`+`listName`) |
176
- | [get_task_time_entries](docs/api-reference.md#time-tracking) | Get time entries for a task | `taskId`/`taskName` |
177
- | [start_time_tracking](docs/api-reference.md#time-tracking) | Start time tracking on a task | `taskId`/`taskName` |
178
- | [stop_time_tracking](docs/api-reference.md#time-tracking) | Stop current time tracking | None |
179
- | [add_time_entry](docs/api-reference.md#time-tracking) | Add manual time entry to a task | `taskId`/`taskName`, `start`, `duration` |
180
- | [delete_time_entry](docs/api-reference.md#time-tracking) | Delete a time entry | `timeEntryId` |
181
- | [get_current_time_entry](docs/api-reference.md#time-tracking) | Get currently running timer | None |
182
- | [get_workspace_members](docs/api-reference.md#member-management) | Get all workspace members | None |
183
- | [find_member_by_name](docs/api-reference.md#member-management) | Find member by name or email | `nameOrEmail` |
184
- | [resolve_assignees](docs/api-reference.md#member-management) | Resolve member names to IDs | `assignees[]` |
185
- | [create_document](docs/api-reference.md#document-management) | Create a document | `workspaceId`, `name`, `parentId`/`parentType`, `visibility`, `create_pages` |
186
- | [get_document](docs/api-reference.md#document-management) | Get a document | `workspaceId`/`documentId` |
187
- | [list_documents](docs/api-reference.md#document-management) | List documents | `workspaceId`, `documentId`/`creator`/`deleted`/`archived`/`parent_id`/`parent_type`/`limit`/`next_cursor` |
188
- | [list_document_pages](docs/api-reference.md#document-management) | List document pages | `documentId`/`documentName` |
189
- | [get_document_pages](docs/api-reference.md#document-management) | Get document pages | `documentId`/`documentName`, `pageIds` |
190
- | [create_document_pages](docs/api-reference.md#document-management) | Create a document page | `workspaceId`/`documentId`, `parent_page_id`/`name`/`sub_title`,`content`/`content_format` |
191
- | [update_document_page](docs/api-reference.md#document-management) | Update a document page | `workspaceId`/`documentId`, `name`/`sub_title`,`content`/`content_edit_mode`/`content_format` |
192
-
193
- See [full documentation](docs/api-reference.md) for optional parameters and advanced usage.
183
+ | [get_workspace_hierarchy](docs/user-guide.md#workspace-navigation) | Get workspace structure | None |
184
+ | [create_task](docs/user-guide.md#task-management) | Create a task | `name`, (`listId`/`listName`) |
185
+ | [create_bulk_tasks](docs/user-guide.md#task-management) | Create multiple tasks | `tasks[]` |
186
+ | [update_task](docs/user-guide.md#task-management) | Modify task | `taskId`/`taskName` |
187
+ | [update_bulk_tasks](docs/user-guide.md#task-management) | Update multiple tasks | `tasks[]` with IDs or names |
188
+ | [get_tasks](docs/user-guide.md#task-management) | Get tasks from list | `listId`/`listName` |
189
+ | [get_task](docs/user-guide.md#task-management) | Get single task details | `taskId`/`taskName` (with smart disambiguation) |
190
+ | [get_workspace_tasks](docs/user-guide.md#task-management) | Get tasks with filtering | At least one filter (tags, list_ids, space_ids, etc.) |
191
+ | [get_task_comments](docs/user-guide.md#task-management) | Get comments on a task | `taskId`/`taskName` |
192
+ | [create_task_comment](docs/user-guide.md#task-management) | Add a comment to a task | `commentText`, (`taskId`/(`taskName`+`listName`)) |
193
+ | [attach_task_file](docs/user-guide.md#task-management) | Attach file to a task | `taskId`/`taskName`, (`file_data` or `file_url`) |
194
+ | [delete_task](docs/user-guide.md#task-management) | Remove task | `taskId`/`taskName` |
195
+ | [delete_bulk_tasks](docs/user-guide.md#task-management) | Remove multiple tasks | `tasks[]` with IDs or names |
196
+ | [move_task](docs/user-guide.md#task-management) | Move task | `taskId`/`taskName`, `listId`/`listName` |
197
+ | [move_bulk_tasks](docs/user-guide.md#task-management) | Move multiple tasks | `tasks[]` with IDs or names, target list |
198
+ | [duplicate_task](docs/user-guide.md#task-management) | Copy task | `taskId`/`taskName`, `listId`/`listName` |
199
+ | [create_list](docs/user-guide.md#list-management) | Create list in space | `name`, `spaceId`/`spaceName` |
200
+ | [create_folder](docs/user-guide.md#folder-management) | Create folder | `name`, `spaceId`/`spaceName` |
201
+ | [create_list_in_folder](docs/user-guide.md#list-management) | Create list in folder | `name`, `folderId`/`folderName` |
202
+ | [get_folder](docs/user-guide.md#folder-management) | Get folder details | `folderId`/`folderName` |
203
+ | [update_folder](docs/user-guide.md#folder-management) | Update folder properties | `folderId`/`folderName` |
204
+ | [delete_folder](docs/user-guide.md#folder-management) | Delete folder | `folderId`/`folderName` |
205
+ | [get_list](docs/user-guide.md#list-management) | Get list details | `listId`/`listName` |
206
+ | [update_list](docs/user-guide.md#list-management) | Update list properties | `listId`/`listName` |
207
+ | [delete_list](docs/user-guide.md#list-management) | Delete list | `listId`/`listName` |
208
+ | [get_space_tags](docs/user-guide.md#tag-management) | Get space tags | `spaceId`/`spaceName` |
209
+ | [create_space_tag](docs/user-guide.md#tag-management) | Create tag | `tagName`, `spaceId`/`spaceName` |
210
+ | [update_space_tag](docs/user-guide.md#tag-management) | Update tag | `tagName`, `spaceId`/`spaceName` |
211
+ | [delete_space_tag](docs/user-guide.md#tag-management) | Delete tag | `tagName`, `spaceId`/`spaceName` |
212
+ | [add_tag_to_task](docs/user-guide.md#tag-management) | Add tag to task | `tagName`, `taskId`/(`taskName`+`listName`) |
213
+ | [remove_tag_from_task](docs/user-guide.md#tag-management) | Remove tag from task | `tagName`, `taskId`/(`taskName`+`listName`) |
214
+ | [get_task_time_entries](docs/user-guide.md#time-tracking) | Get time entries for a task | `taskId`/`taskName` |
215
+ | [start_time_tracking](docs/user-guide.md#time-tracking) | Start time tracking on a task | `taskId`/`taskName` |
216
+ | [stop_time_tracking](docs/user-guide.md#time-tracking) | Stop current time tracking | None |
217
+ | [add_time_entry](docs/user-guide.md#time-tracking) | Add manual time entry to a task | `taskId`/`taskName`, `start`, `duration` |
218
+ | [delete_time_entry](docs/user-guide.md#time-tracking) | Delete a time entry | `timeEntryId` |
219
+ | [get_current_time_entry](docs/user-guide.md#time-tracking) | Get currently running timer | None |
220
+ | [get_workspace_members](docs/user-guide.md#member-management) | Get all workspace members | None |
221
+ | [find_member_by_name](docs/user-guide.md#member-management) | Find member by name or email | `nameOrEmail` |
222
+ | [resolve_assignees](docs/user-guide.md#member-management) | Resolve member names to IDs | `assignees[]` |
223
+ | [create_document](docs/user-guide.md#document-management) | Create a document | `workspaceId`, `name`, `parentId`/`parentType`, `visibility`, `create_pages` |
224
+ | [get_document](docs/user-guide.md#document-management) | Get a document | `workspaceId`/`documentId` |
225
+ | [list_documents](docs/user-guide.md#document-management) | List documents | `workspaceId`, `documentId`/`creator`/`deleted`/`archived`/`parent_id`/`parent_type`/`limit`/`next_cursor` |
226
+ | [list_document_pages](docs/user-guide.md#document-management) | List document pages | `documentId`/`documentName` |
227
+ | [get_document_pages](docs/user-guide.md#document-management) | Get document pages | `documentId`/`documentName`, `pageIds` |
228
+ | [create_document_pages](docs/user-guide.md#document-management) | Create a document page | `workspaceId`/`documentId`, `parent_page_id`/`name`/`sub_title`,`content`/`content_format` |
229
+ | [update_document_page](docs/user-guide.md#document-management) | Update a document page | `workspaceId`/`documentId`, `name`/`sub_title`,`content`/`content_edit_mode`/`content_format` |
230
+
231
+ See [full documentation](docs/user-guide.md) for optional parameters and advanced usage.
194
232
 
195
233
  ## Member Management Tools
196
234
 
@@ -221,9 +259,9 @@ Not yet implemented and not supported by all client apps. Request a feature for
221
259
 
222
260
  | Prompt | Purpose | Features |
223
261
  | -------------------------------------------------- | ------------------------- | ----------------------------------------- |
224
- | [summarize_tasks](docs/api-reference.md#prompts) | Task overview | Status summary, priorities, relationships |
225
- | [analyze_priorities](docs/api-reference.md#prompts) | Priority optimization | Distribution analysis, sequencing |
226
- | [generate_description](docs/api-reference.md#prompts) | Task description creation | Objectives, criteria, dependencies |
262
+ | [summarize_tasks](docs/user-guide.md#prompts) | Task overview | Status summary, priorities, relationships |
263
+ | [analyze_priorities](docs/user-guide.md#prompts) | Priority optimization | Distribution analysis, sequencing |
264
+ | [generate_description](docs/user-guide.md#prompts) | Task description creation | Objectives, criteria, dependencies |
227
265
 
228
266
  ## Error Handling
229
267
 
@@ -270,4 +308,4 @@ This software makes use of third-party APIs and may reference trademarks
270
308
  or brands owned by third parties. The use of such APIs or references does not imply
271
309
  any affiliation with or endorsement by the respective companies. All trademarks and
272
310
  brand names are the property of their respective owners. This project is an independent
273
- work and is not officially associated with or sponsored by any third-party company mentioned.
311
+ work and is not officially associated with or sponsored by any third-party company mentioned.
package/build/config.js CHANGED
@@ -12,6 +12,10 @@
12
12
  * The default value is 'false' (string), which means document support will be disabled if
13
13
  * no parameter is passed. Pass it as 'true' (string) to enable it.
14
14
  *
15
+ * Tool filtering options:
16
+ * - ENABLED_TOOLS: Comma-separated list of tools to enable (takes precedence over DISABLED_TOOLS)
17
+ * - DISABLED_TOOLS: Comma-separated list of tools to disable (ignored if ENABLED_TOOLS is specified)
18
+ *
15
19
  * Server transport options:
16
20
  * - ENABLE_SSE: Enable Server-Sent Events transport (default: false)
17
21
  * - SSE_PORT: Port for SSE server (default: 3000)
@@ -29,16 +33,12 @@ for (let i = 0; i < args.length; i++) {
29
33
  envArgs.clickupTeamId = value;
30
34
  if (key === 'DOCUMENT_SUPPORT')
31
35
  envArgs.documentSupport = value;
32
- if (key === 'DOCUMENT_MODEL')
33
- envArgs.documentSupport = value; // Backward compatibility
34
- if (key === 'DOCUMENT_MODULE')
35
- envArgs.documentSupport = value; // Backward compatibility
36
36
  if (key === 'LOG_LEVEL')
37
37
  envArgs.logLevel = value;
38
38
  if (key === 'DISABLED_TOOLS')
39
39
  envArgs.disabledTools = value;
40
- if (key === 'DISABLED_COMMANDS')
41
- envArgs.disabledTools = value; // Backward compatibility
40
+ if (key === 'ENABLED_TOOLS')
41
+ envArgs.enabledTools = value;
42
42
  if (key === 'ENABLE_SSE')
43
43
  envArgs.enableSSE = value;
44
44
  if (key === 'SSE_PORT')
@@ -60,7 +60,7 @@ export var LogLevel;
60
60
  LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
61
61
  })(LogLevel || (LogLevel = {}));
62
62
  // Parse LOG_LEVEL string to LogLevel enum
63
- export const parseLogLevel = (levelStr) => {
63
+ const parseLogLevel = (levelStr) => {
64
64
  if (!levelStr)
65
65
  return LogLevel.ERROR; // Default to ERROR if not specified
66
66
  switch (levelStr.toUpperCase()) {
@@ -95,6 +95,7 @@ const configuration = {
95
95
  documentSupport: envArgs.documentSupport || process.env.DOCUMENT_SUPPORT || process.env.DOCUMENT_MODULE || process.env.DOCUMENT_MODEL || 'false',
96
96
  logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL),
97
97
  disabledTools: ((envArgs.disabledTools || process.env.DISABLED_TOOLS || process.env.DISABLED_COMMANDS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
98
+ enabledTools: ((envArgs.enabledTools || process.env.ENABLED_TOOLS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
98
99
  enableSSE: parseBoolean(envArgs.enableSSE || process.env.ENABLE_SSE, false),
99
100
  ssePort: parseInteger(envArgs.ssePort || process.env.SSE_PORT, 3000),
100
101
  enableStdio: parseBoolean(envArgs.enableStdio || process.env.ENABLE_STDIO, true),
package/build/logger.js CHANGED
@@ -29,7 +29,7 @@ export { LogLevel };
29
29
  * @param level The log level to check
30
30
  * @returns True if the level should be logged
31
31
  */
32
- export function isLevelEnabled(level) {
32
+ function isLevelEnabled(level) {
33
33
  return level >= configuredLevel;
34
34
  }
35
35
  /**
@@ -38,7 +38,7 @@ export function isLevelEnabled(level) {
38
38
  * @param message Message to log
39
39
  * @param data Optional data to include in log
40
40
  */
41
- export function log(level, message, data) {
41
+ function log(level, message, data) {
42
42
  const levelEnum = level === 'trace' ? LogLevel.TRACE
43
43
  : level === 'debug' ? LogLevel.DEBUG
44
44
  : level === 'info' ? LogLevel.INFO
@@ -78,22 +78,6 @@ export function log(level, message, data) {
78
78
  // Write to file only, not to stderr which would interfere with JSON-RPC
79
79
  logStream.write(logMessage + '\n');
80
80
  }
81
- /**
82
- * Shorthand for trace level logs
83
- * @param message Message to log
84
- * @param data Optional data to include in log
85
- */
86
- export function trace(message, data) {
87
- log('trace', message, data);
88
- }
89
- /**
90
- * Shorthand for debug level logs
91
- * @param message Message to log
92
- * @param data Optional data to include in log
93
- */
94
- export function debug(message, data) {
95
- log('debug', message, data);
96
- }
97
81
  /**
98
82
  * Shorthand for info level logs
99
83
  * @param message Message to log
@@ -102,14 +86,6 @@ export function debug(message, data) {
102
86
  export function info(message, data) {
103
87
  log('info', message, data);
104
88
  }
105
- /**
106
- * Shorthand for warn level logs
107
- * @param message Message to log
108
- * @param data Optional data to include in log
109
- */
110
- export function warn(message, data) {
111
- log('warn', message, data);
112
- }
113
89
  /**
114
90
  * Shorthand for error level logs
115
91
  * @param message Message to log
package/build/server.js CHANGED
@@ -20,9 +20,32 @@ import { clickUpServices } from "./services/shared.js";
20
20
  const logger = new Logger('Server');
21
21
  // Use existing services from shared module instead of creating new ones
22
22
  const { workspace } = clickUpServices;
23
+ /**
24
+ * Determines if a tool should be enabled based on ENABLED_TOOLS and DISABLED_TOOLS configuration.
25
+ *
26
+ * Logic:
27
+ * 1. If ENABLED_TOOLS is specified, only tools in that list are enabled (ENABLED_TOOLS takes precedence)
28
+ * 2. If ENABLED_TOOLS is not specified but DISABLED_TOOLS is, all tools except those in DISABLED_TOOLS are enabled
29
+ * 3. If neither is specified, all tools are enabled
30
+ *
31
+ * @param toolName - The name of the tool to check
32
+ * @returns true if the tool should be enabled, false otherwise
33
+ */
34
+ const isToolEnabled = (toolName) => {
35
+ // If ENABLED_TOOLS is specified, it takes precedence
36
+ if (config.enabledTools.length > 0) {
37
+ return config.enabledTools.includes(toolName);
38
+ }
39
+ // If only DISABLED_TOOLS is specified, enable all tools except those disabled
40
+ if (config.disabledTools.length > 0) {
41
+ return !config.disabledTools.includes(toolName);
42
+ }
43
+ // If neither is specified, enable all tools
44
+ return true;
45
+ };
23
46
  export const server = new Server({
24
47
  name: "clickup-mcp-server",
25
- version: "0.8.1",
48
+ version: "0.8.3",
26
49
  }, {
27
50
  capabilities: {
28
51
  tools: {},
@@ -93,7 +116,7 @@ export function configureServer() {
93
116
  findMemberByNameTool,
94
117
  resolveAssigneesTool,
95
118
  ...documentModule()
96
- ].filter(tool => !config.disabledTools.includes(tool.name))
119
+ ].filter(tool => isToolEnabled(tool.name))
97
120
  };
98
121
  });
99
122
  // Add handler for resources/list
@@ -112,12 +135,15 @@ export function configureServer() {
112
135
  logger.info(`Received CallTool request for tool: ${name}`, {
113
136
  params
114
137
  });
115
- // Check if the tool is disabled
116
- if (config.disabledTools.includes(name)) {
117
- logger.warn(`Tool execution blocked: Tool '${name}' is disabled.`);
138
+ // Check if the tool is enabled
139
+ if (!isToolEnabled(name)) {
140
+ const reason = config.enabledTools.length > 0
141
+ ? `Tool '${name}' is not in the enabled tools list.`
142
+ : `Tool '${name}' is disabled.`;
143
+ logger.warn(`Tool execution blocked: ${reason}`);
118
144
  throw {
119
145
  code: -32601,
120
- message: `Tool '${name}' is disabled.`
146
+ message: reason
121
147
  };
122
148
  }
123
149
  try {
@@ -59,6 +59,8 @@ export class TaskServiceCore extends BaseClickUpService {
59
59
  params.append('include_closed', String(filters.include_closed));
60
60
  if (filters.subtasks)
61
61
  params.append('subtasks', String(filters.subtasks));
62
+ if (filters.include_subtasks)
63
+ params.append('include_subtasks', String(filters.include_subtasks));
62
64
  if (filters.page)
63
65
  params.append('page', String(filters.page));
64
66
  if (filters.order_by)
@@ -193,6 +193,169 @@ export class TaskServiceSearch extends TaskServiceCore {
193
193
  async getTaskSummaries(filters = {}) {
194
194
  return this.getWorkspaceTasks({ ...filters, detail_level: 'summary' });
195
195
  }
196
+ /**
197
+ * Get all views for a given list and identify the default "List" view ID
198
+ * @param listId The ID of the list to get views for
199
+ * @returns The ID of the default list view, or null if not found
200
+ */
201
+ async getListViews(listId) {
202
+ try {
203
+ this.logOperation('getListViews', { listId });
204
+ const response = await this.makeRequest(async () => {
205
+ return await this.client.get(`/list/${listId}/view`);
206
+ });
207
+ // First try to get the default list view from required_views.list
208
+ if (response.data.required_views?.list?.id) {
209
+ this.logOperation('getListViews', {
210
+ listId,
211
+ foundDefaultView: response.data.required_views.list.id,
212
+ source: 'required_views.list'
213
+ });
214
+ return response.data.required_views.list.id;
215
+ }
216
+ // Fallback: look for a view with type "list" in the views array
217
+ const listView = response.data.views?.find(view => view.type?.toLowerCase() === 'list' ||
218
+ view.name?.toLowerCase().includes('list'));
219
+ if (listView?.id) {
220
+ this.logOperation('getListViews', {
221
+ listId,
222
+ foundDefaultView: listView.id,
223
+ source: 'views_array_fallback',
224
+ viewName: listView.name
225
+ });
226
+ return listView.id;
227
+ }
228
+ // If no specific list view found, use the first available view
229
+ if (response.data.views?.length > 0) {
230
+ const firstView = response.data.views[0];
231
+ this.logOperation('getListViews', {
232
+ listId,
233
+ foundDefaultView: firstView.id,
234
+ source: 'first_available_view',
235
+ viewName: firstView.name,
236
+ warning: 'No specific list view found, using first available view'
237
+ });
238
+ return firstView.id;
239
+ }
240
+ this.logOperation('getListViews', {
241
+ listId,
242
+ error: 'No views found for list',
243
+ responseData: response.data
244
+ });
245
+ return null;
246
+ }
247
+ catch (error) {
248
+ this.logOperation('getListViews', {
249
+ listId,
250
+ error: error.message,
251
+ status: error.response?.status
252
+ });
253
+ throw this.handleError(error, `Failed to get views for list ${listId}`);
254
+ }
255
+ }
256
+ /**
257
+ * Retrieve tasks from a specific view, applying supported filters
258
+ * @param viewId The ID of the view to get tasks from
259
+ * @param filters Task filters to apply (only supported filters will be used)
260
+ * @returns Array of ClickUpTask objects from the view
261
+ */
262
+ async getTasksFromView(viewId, filters = {}) {
263
+ try {
264
+ this.logOperation('getTasksFromView', { viewId, filters });
265
+ // Build query parameters for supported filters
266
+ const params = {};
267
+ // Map supported filters to query parameters
268
+ if (filters.subtasks !== undefined)
269
+ params.subtasks = filters.subtasks;
270
+ if (filters.include_closed !== undefined)
271
+ params.include_closed = filters.include_closed;
272
+ if (filters.archived !== undefined)
273
+ params.archived = filters.archived;
274
+ if (filters.page !== undefined)
275
+ params.page = filters.page;
276
+ if (filters.order_by)
277
+ params.order_by = filters.order_by;
278
+ if (filters.reverse !== undefined)
279
+ params.reverse = filters.reverse;
280
+ // Status filtering
281
+ if (filters.statuses && filters.statuses.length > 0) {
282
+ params.statuses = filters.statuses;
283
+ }
284
+ // Assignee filtering
285
+ if (filters.assignees && filters.assignees.length > 0) {
286
+ params.assignees = filters.assignees;
287
+ }
288
+ // Date filters
289
+ if (filters.date_created_gt)
290
+ params.date_created_gt = filters.date_created_gt;
291
+ if (filters.date_created_lt)
292
+ params.date_created_lt = filters.date_created_lt;
293
+ if (filters.date_updated_gt)
294
+ params.date_updated_gt = filters.date_updated_gt;
295
+ if (filters.date_updated_lt)
296
+ params.date_updated_lt = filters.date_updated_lt;
297
+ if (filters.due_date_gt)
298
+ params.due_date_gt = filters.due_date_gt;
299
+ if (filters.due_date_lt)
300
+ params.due_date_lt = filters.due_date_lt;
301
+ // Custom fields
302
+ if (filters.custom_fields) {
303
+ params.custom_fields = filters.custom_fields;
304
+ }
305
+ let allTasks = [];
306
+ let currentPage = filters.page || 0;
307
+ let hasMore = true;
308
+ const maxPages = 50; // Safety limit to prevent infinite loops
309
+ let pageCount = 0;
310
+ while (hasMore && pageCount < maxPages) {
311
+ const pageParams = { ...params, page: currentPage };
312
+ const response = await this.makeRequest(async () => {
313
+ return await this.client.get(`/view/${viewId}/task`, {
314
+ params: pageParams
315
+ });
316
+ });
317
+ const tasks = response.data.tasks || [];
318
+ allTasks = allTasks.concat(tasks);
319
+ // Check if there are more pages
320
+ hasMore = response.data.has_more === true && tasks.length > 0;
321
+ currentPage++;
322
+ pageCount++;
323
+ this.logOperation('getTasksFromView', {
324
+ viewId,
325
+ page: currentPage - 1,
326
+ tasksInPage: tasks.length,
327
+ totalTasksSoFar: allTasks.length,
328
+ hasMore
329
+ });
330
+ // If we're not paginating (original request had no page specified),
331
+ // only get the first page
332
+ if (filters.page === undefined && currentPage === 1) {
333
+ break;
334
+ }
335
+ }
336
+ if (pageCount >= maxPages) {
337
+ this.logOperation('getTasksFromView', {
338
+ viewId,
339
+ warning: `Reached maximum page limit (${maxPages}) while fetching tasks`,
340
+ totalTasks: allTasks.length
341
+ });
342
+ }
343
+ this.logOperation('getTasksFromView', {
344
+ viewId,
345
+ totalTasks: allTasks.length,
346
+ totalPages: pageCount
347
+ });
348
+ return allTasks;
349
+ }
350
+ catch (error) {
351
+ this.logOperation('getTasksFromView', {
352
+ viewId,
353
+ error: error.message,
354
+ status: error.response?.status
355
+ });
356
+ throw this.handleError(error, `Failed to get tasks from view ${viewId}`);
357
+ }
358
+ }
196
359
  /**
197
360
  * Get detailed task data
198
361
  * @param filters Task filters to apply
@@ -480,12 +480,3 @@ export async function handleUpdateDocumentPage(parameters) {
480
480
  return sponsorService.createErrorResponse(`Failed to update document page: ${error.message}`);
481
481
  }
482
482
  }
483
- export const documentTools = [
484
- createDocumentTool,
485
- getDocumentTool,
486
- listDocumentsTool,
487
- listDocumentPagesTool,
488
- getDocumentPagesTool,
489
- createDocumentPageTool,
490
- updateDocumentPageTool
491
- ];
@@ -12,6 +12,7 @@ import { clickUpServices } from '../../services/shared.js';
12
12
  import { BulkService } from '../../services/clickup/bulk.js';
13
13
  import { parseDueDate } from '../utils.js';
14
14
  import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
15
+ import { handleResolveAssignees } from '../member.js';
15
16
  import { workspaceService } from '../../services/shared.js';
16
17
  import { isNameMatch } from '../../utils/resolver-utils.js';
17
18
  import { Logger } from '../../logger.js';
@@ -79,10 +80,55 @@ function parseTimeEstimate(timeEstimate) {
79
80
  }
80
81
  return Math.round(totalMinutes); // Return minutes
81
82
  }
83
+ /**
84
+ * Resolve assignees from mixed input (user IDs, emails, usernames) to user IDs
85
+ */
86
+ async function resolveAssignees(assignees) {
87
+ if (!assignees || !Array.isArray(assignees) || assignees.length === 0) {
88
+ return [];
89
+ }
90
+ const resolved = [];
91
+ const toResolve = [];
92
+ // Separate numeric IDs from strings that need resolution
93
+ for (const assignee of assignees) {
94
+ if (typeof assignee === 'number') {
95
+ resolved.push(assignee);
96
+ }
97
+ else if (typeof assignee === 'string') {
98
+ // Check if it's a numeric string
99
+ const numericId = parseInt(assignee, 10);
100
+ if (!isNaN(numericId) && numericId.toString() === assignee) {
101
+ resolved.push(numericId);
102
+ }
103
+ else {
104
+ // It's an email or username that needs resolution
105
+ toResolve.push(assignee);
106
+ }
107
+ }
108
+ }
109
+ // Resolve emails/usernames to user IDs if any
110
+ if (toResolve.length > 0) {
111
+ try {
112
+ const result = await handleResolveAssignees({ assignees: toResolve });
113
+ if (result.userIds && Array.isArray(result.userIds)) {
114
+ for (const userId of result.userIds) {
115
+ if (userId !== null && typeof userId === 'number') {
116
+ resolved.push(userId);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.warn('Failed to resolve some assignees:', error.message);
123
+ // Continue with the IDs we could resolve
124
+ }
125
+ }
126
+ return resolved;
127
+ }
82
128
  /**
83
129
  * Build task update data from parameters
84
130
  */
85
- function buildUpdateData(params) {
131
+ async function buildUpdateData(params) {
86
132
  const updateData = {};
87
133
  if (params.name !== undefined)
88
134
  updateData.name = params.name;
@@ -116,6 +162,10 @@ function buildUpdateData(params) {
116
162
  if (params.custom_fields !== undefined) {
117
163
  updateData.custom_fields = params.custom_fields;
118
164
  }
165
+ // Handle assignees if provided - resolve emails/usernames to user IDs
166
+ if (params.assignees !== undefined) {
167
+ updateData.assignees = await resolveAssignees(params.assignees);
168
+ }
119
169
  return updateData;
120
170
  }
121
171
  /**
@@ -377,18 +427,23 @@ export async function createTaskHandler(params) {
377
427
  // Use our helper function to validate and convert priority
378
428
  const priority = toTaskPriority(params.priority);
379
429
  const listId = await getListId(params.listId, params.listName);
430
+ // Resolve assignees if provided
431
+ const resolvedAssignees = assignees ? await resolveAssignees(assignees) : undefined;
380
432
  const taskData = {
381
433
  name,
382
434
  description,
383
435
  markdown_description,
384
436
  status,
385
- priority,
386
437
  parent,
387
438
  tags,
388
439
  custom_fields,
389
440
  check_required_custom_fields,
390
- assignees
441
+ assignees: resolvedAssignees
391
442
  };
443
+ // Only include priority if explicitly provided by the user
444
+ if (priority !== undefined) {
445
+ taskData.priority = priority;
446
+ }
392
447
  // Add due date if specified
393
448
  if (dueDate) {
394
449
  taskData.due_date = parseDueDate(dueDate);
@@ -405,12 +460,14 @@ export async function createTaskHandler(params) {
405
460
  * Handler for updating a task
406
461
  */
407
462
  export async function updateTaskHandler(taskService, params) {
408
- const { taskId, taskName, listName, customTaskId, ...updateData } = params;
463
+ const { taskId, taskName, listName, customTaskId, ...rawUpdateData } = params;
409
464
  // Validate task identification with global lookup enabled
410
465
  const validationResult = validateTaskIdentification(params, { useGlobalLookup: true });
411
466
  if (!validationResult.isValid) {
412
467
  throw new Error(validationResult.errorMessage);
413
468
  }
469
+ // Build properly formatted update data from raw parameters (now async)
470
+ const updateData = await buildUpdateData(rawUpdateData);
414
471
  // Validate update data
415
472
  validateTaskUpdateData(updateData);
416
473
  try {
@@ -542,8 +599,135 @@ export async function getWorkspaceTasksHandler(taskService, params) {
542
599
  if (!hasFilter) {
543
600
  throw new Error('At least one filter parameter is required (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)');
544
601
  }
545
- // For workspace tasks, we'll continue to use the direct getWorkspaceTasks method
546
- // since it supports specific workspace-wide filters that aren't part of the unified findTasks
602
+ // Check if list_ids are provided for enhanced filtering via Views API
603
+ if (params.list_ids && params.list_ids.length > 0) {
604
+ logger.info('Using Views API for enhanced list filtering', {
605
+ listIds: params.list_ids,
606
+ listCount: params.list_ids.length
607
+ });
608
+ // Warning for broad queries
609
+ const hasOnlyListIds = Object.keys(params).filter(key => params[key] !== undefined && key !== 'list_ids' && key !== 'detail_level').length === 0;
610
+ if (hasOnlyListIds && params.list_ids.length > 5) {
611
+ logger.warn('Broad query detected: many lists with no additional filters', {
612
+ listCount: params.list_ids.length,
613
+ recommendation: 'Consider adding additional filters (tags, statuses, assignees, etc.) for better performance'
614
+ });
615
+ }
616
+ // Use Views API for enhanced list filtering
617
+ let allTasks = [];
618
+ const processedTaskIds = new Set();
619
+ // Create promises for concurrent fetching
620
+ const fetchPromises = params.list_ids.map(async (listId) => {
621
+ try {
622
+ // Get the default list view ID
623
+ const viewId = await taskService.getListViews(listId);
624
+ if (!viewId) {
625
+ logger.warn(`No default view found for list ${listId}, skipping`);
626
+ return [];
627
+ }
628
+ // Extract filters supported by the Views API
629
+ const supportedFilters = {
630
+ subtasks: params.subtasks,
631
+ include_closed: params.include_closed,
632
+ archived: params.archived,
633
+ order_by: params.order_by,
634
+ reverse: params.reverse,
635
+ page: params.page,
636
+ statuses: params.statuses,
637
+ assignees: params.assignees,
638
+ date_created_gt: params.date_created_gt,
639
+ date_created_lt: params.date_created_lt,
640
+ date_updated_gt: params.date_updated_gt,
641
+ date_updated_lt: params.date_updated_lt,
642
+ due_date_gt: params.due_date_gt,
643
+ due_date_lt: params.due_date_lt,
644
+ custom_fields: params.custom_fields
645
+ };
646
+ // Get tasks from the view
647
+ const tasksFromView = await taskService.getTasksFromView(viewId, supportedFilters);
648
+ return tasksFromView;
649
+ }
650
+ catch (error) {
651
+ logger.error(`Failed to get tasks from list ${listId}`, { error: error.message });
652
+ return []; // Continue with other lists even if one fails
653
+ }
654
+ });
655
+ // Execute all fetches concurrently
656
+ const taskArrays = await Promise.all(fetchPromises);
657
+ // Aggregate tasks and remove duplicates
658
+ for (const tasks of taskArrays) {
659
+ for (const task of tasks) {
660
+ if (!processedTaskIds.has(task.id)) {
661
+ allTasks.push(task);
662
+ processedTaskIds.add(task.id);
663
+ }
664
+ }
665
+ }
666
+ logger.info('Aggregated tasks from Views API', {
667
+ totalTasks: allTasks.length,
668
+ uniqueTasks: processedTaskIds.size
669
+ });
670
+ // Apply client-side filtering for unsupported filters
671
+ if (params.tags && params.tags.length > 0) {
672
+ allTasks = allTasks.filter(task => params.tags.every((tag) => task.tags.some(t => t.name === tag)));
673
+ logger.debug('Applied client-side tag filtering', {
674
+ tags: params.tags,
675
+ remainingTasks: allTasks.length
676
+ });
677
+ }
678
+ if (params.folder_ids && params.folder_ids.length > 0) {
679
+ allTasks = allTasks.filter(task => task.folder && params.folder_ids.includes(task.folder.id));
680
+ logger.debug('Applied client-side folder filtering', {
681
+ folderIds: params.folder_ids,
682
+ remainingTasks: allTasks.length
683
+ });
684
+ }
685
+ if (params.space_ids && params.space_ids.length > 0) {
686
+ allTasks = allTasks.filter(task => params.space_ids.includes(task.space.id));
687
+ logger.debug('Applied client-side space filtering', {
688
+ spaceIds: params.space_ids,
689
+ remainingTasks: allTasks.length
690
+ });
691
+ }
692
+ // Check token limit and format response
693
+ const shouldUseSummary = params.detail_level === 'summary' || wouldExceedTokenLimit({ tasks: allTasks });
694
+ if (shouldUseSummary) {
695
+ logger.info('Using summary format for Views API response', {
696
+ totalTasks: allTasks.length,
697
+ reason: params.detail_level === 'summary' ? 'requested' : 'token_limit'
698
+ });
699
+ return {
700
+ summaries: allTasks.map(task => ({
701
+ id: task.id,
702
+ name: task.name,
703
+ status: task.status.status,
704
+ list: {
705
+ id: task.list.id,
706
+ name: task.list.name
707
+ },
708
+ due_date: task.due_date,
709
+ url: task.url,
710
+ priority: task.priority?.priority || null,
711
+ tags: task.tags.map(tag => ({
712
+ name: tag.name,
713
+ tag_bg: tag.tag_bg,
714
+ tag_fg: tag.tag_fg
715
+ }))
716
+ })),
717
+ total_count: allTasks.length,
718
+ has_more: false,
719
+ next_page: 0
720
+ };
721
+ }
722
+ return {
723
+ tasks: allTasks,
724
+ total_count: allTasks.length,
725
+ has_more: false,
726
+ next_page: 0
727
+ };
728
+ }
729
+ // Fallback to existing workspace-wide task retrieval when list_ids are not provided
730
+ logger.info('Using standard workspace task retrieval');
547
731
  const filters = {
548
732
  tags: params.tags,
549
733
  list_ids: params.list_ids,
@@ -564,7 +748,11 @@ export async function getWorkspaceTasksHandler(taskService, params) {
564
748
  date_updated_lt: params.date_updated_lt,
565
749
  assignees: params.assignees,
566
750
  page: params.page,
567
- detail_level: params.detail_level || 'detailed'
751
+ detail_level: params.detail_level || 'detailed',
752
+ subtasks: params.subtasks,
753
+ include_subtasks: params.include_subtasks,
754
+ include_compact_time_entries: params.include_compact_time_entries,
755
+ custom_fields: params.custom_fields
568
756
  };
569
757
  // Get tasks with adaptive response format support
570
758
  const response = await taskService.getWorkspaceTasks(filters);
@@ -597,18 +785,24 @@ export async function createBulkTasksHandler(params) {
597
785
  validateBulkTasks(tasks, 'create');
598
786
  // Validate and resolve list ID
599
787
  const targetListId = await resolveListIdWithValidation(listId, listName);
600
- // Format tasks for creation
601
- const formattedTasks = tasks.map(task => {
788
+ // Format tasks for creation - resolve assignees for each task
789
+ const formattedTasks = await Promise.all(tasks.map(async (task) => {
790
+ // Resolve assignees if provided
791
+ const resolvedAssignees = task.assignees ? await resolveAssignees(task.assignees) : undefined;
602
792
  const taskData = {
603
793
  name: task.name,
604
794
  description: task.description,
605
795
  markdown_description: task.markdown_description,
606
796
  status: task.status,
607
- priority: toTaskPriority(task.priority),
608
797
  tags: task.tags,
609
798
  custom_fields: task.custom_fields,
610
- assignees: task.assignees
799
+ assignees: resolvedAssignees
611
800
  };
801
+ // Only include priority if explicitly provided by the user
802
+ const priority = toTaskPriority(task.priority);
803
+ if (priority !== undefined) {
804
+ taskData.priority = priority;
805
+ }
612
806
  // Add due date if specified
613
807
  if (task.dueDate) {
614
808
  taskData.due_date = parseDueDate(task.dueDate);
@@ -620,7 +814,7 @@ export async function createBulkTasksHandler(params) {
620
814
  taskData.start_date_time = true;
621
815
  }
622
816
  return taskData;
623
- });
817
+ }));
624
818
  // Parse bulk options
625
819
  const bulkOptions = parseBulkOptions(options);
626
820
  // Create tasks - pass arguments in correct order: listId, tasks, options
@@ -17,6 +17,7 @@ export const getWorkspaceTasksTool = {
17
17
  Valid Usage:
18
18
  1. Apply any combination of filters (tags, lists, folders, spaces, statuses, etc.)
19
19
  2. Use pagination to manage large result sets
20
+ 3. Include subtasks by setting subtasks=true
20
21
 
21
22
  Requirements:
22
23
  - At least one filter parameter is REQUIRED (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)
@@ -28,10 +29,13 @@ Notes:
28
29
  - Tag filtering is especially useful for cross-list organization (e.g., "project-x", "blocker", "needs-review")
29
30
  - Combine multiple filters to narrow down your search scope
30
31
  - Use pagination for large result sets
32
+ - Set subtasks=true to include subtask details in the response
33
+ IMPORTANT: subtasks=true enables subtasks to appear in results, but subtasks must still match your other filter criteria (tags, lists, etc.) to be returned. To see all subtasks of a specific task regardless of filters, use the get_task tool with subtasks=true instead.
31
34
  - Use the detail_level parameter to control the amount of data returned:
32
35
  - "summary": Returns lightweight task data (name, status, list, tags)
33
36
  - "detailed": Returns complete task data with all fields (DEFAULT if not specified)
34
37
  - Responses exceeding 50,000 tokens automatically switch to summary format to avoid hitting LLM token limits
38
+ - **Enhanced List Filtering**: When list_ids are provided, the tool leverages ClickUp's Views API to include tasks that are *associated with* the specified lists, including tasks that have been added to multiple lists. This provides comprehensive coverage of all tasks related to your specified lists, not just tasks that were originally created in those lists.
35
39
  `,
36
40
  parameters: {
37
41
  type: 'object',
@@ -123,6 +127,22 @@ Notes:
123
127
  type: 'string',
124
128
  enum: ['summary', 'detailed'],
125
129
  description: 'Level of detail to return. Use summary for lightweight responses or detailed for full task data. If not specified, defaults to "detailed".'
130
+ },
131
+ subtasks: {
132
+ type: 'boolean',
133
+ description: 'Include subtasks in the response. Set to true to retrieve subtask details for all returned tasks. Note: subtasks must still match your other filter criteria to appear in results.'
134
+ },
135
+ include_subtasks: {
136
+ type: 'boolean',
137
+ description: 'Alternative parameter for including subtasks (legacy support).'
138
+ },
139
+ include_compact_time_entries: {
140
+ type: 'boolean',
141
+ description: 'Include compact time entry data in the response.'
142
+ },
143
+ custom_fields: {
144
+ type: 'object',
145
+ description: 'Filter by custom field values. Provide as key-value pairs where keys are custom field IDs.'
126
146
  }
127
147
  }
128
148
  },
@@ -216,6 +236,22 @@ Notes:
216
236
  type: 'string',
217
237
  enum: ['summary', 'detailed'],
218
238
  description: 'Level of detail to return. Use summary for lightweight responses or detailed for full task data. If not specified, defaults to "detailed".'
239
+ },
240
+ subtasks: {
241
+ type: 'boolean',
242
+ description: 'Include subtasks in the response. Set to true to retrieve subtask details for all returned tasks. Note: subtasks must still match your other filter criteria to appear in results.'
243
+ },
244
+ include_subtasks: {
245
+ type: 'boolean',
246
+ description: 'Alternative parameter for including subtasks (legacy support).'
247
+ },
248
+ include_compact_time_entries: {
249
+ type: 'boolean',
250
+ description: 'Include compact time entry data in the response.'
251
+ },
252
+ custom_fields: {
253
+ type: 'object',
254
+ description: 'Filter by custom field values. Provide as key-value pairs where keys are custom field IDs.'
219
255
  }
220
256
  }
221
257
  }
@@ -86,7 +86,7 @@ const COLOR_VARIATIONS = {
86
86
  * @param text - Natural language text that contains a color reference
87
87
  * @returns The extracted color name or null if no color is found
88
88
  */
89
- export function extractColorFromText(text) {
89
+ function extractColorFromText(text) {
90
90
  if (!text)
91
91
  return null;
92
92
  // Convert to lowercase for case-insensitive matching
@@ -115,7 +115,7 @@ export function extractColorFromText(text) {
115
115
  * @param colorName - Name of the color to convert (e.g., "blue", "dark red")
116
116
  * @returns HEX color code or null if color name is not recognized
117
117
  */
118
- export function colorNameToHex(colorName) {
118
+ function colorNameToHex(colorName) {
119
119
  if (!colorName)
120
120
  return null;
121
121
  const lowercaseColor = colorName.toLowerCase();
@@ -154,7 +154,7 @@ function calculateLuminance(hex) {
154
154
  * @param backgroundColor - HEX code of the background color
155
155
  * @returns HEX code of the foreground color (either black or white)
156
156
  */
157
- export function generateContrastingForeground(backgroundColor) {
157
+ function generateContrastingForeground(backgroundColor) {
158
158
  const luminance = calculateLuminance(backgroundColor);
159
159
  // Use white text on dark backgrounds and black text on light backgrounds
160
160
  // The threshold 0.5 is based on WCAG guidelines for contrast
@@ -37,7 +37,7 @@ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0
37
37
  * Get the start of today (midnight) in Unix milliseconds
38
38
  * @returns Timestamp in milliseconds for start of current day
39
39
  */
40
- export function getStartOfDay() {
40
+ function getStartOfDay() {
41
41
  const now = new Date();
42
42
  now.setHours(0, 0, 0, 0);
43
43
  return now.getTime();
@@ -46,7 +46,7 @@ export function getStartOfDay() {
46
46
  * Get the end of today (23:59:59.999) in Unix milliseconds
47
47
  * @returns Timestamp in milliseconds for end of current day
48
48
  */
49
- export function getEndOfDay() {
49
+ function getEndOfDay() {
50
50
  const now = new Date();
51
51
  now.setHours(23, 59, 59, 999);
52
52
  return now.getTime();
@@ -55,7 +55,7 @@ export function getEndOfDay() {
55
55
  * Get the current time in Unix milliseconds
56
56
  * @returns Current timestamp in milliseconds
57
57
  */
58
- export function getCurrentTimestamp() {
58
+ function getCurrentTimestamp() {
59
59
  return new Date().getTime();
60
60
  }
61
61
  /**
@@ -69,6 +69,14 @@ export function parseDueDate(dateString) {
69
69
  if (!dateString)
70
70
  return undefined;
71
71
  try {
72
+ // First, try to parse as a direct timestamp
73
+ const numericValue = Number(dateString);
74
+ if (!isNaN(numericValue) && numericValue > 0) {
75
+ // If it's a reasonable timestamp (after year 2000), use it
76
+ if (numericValue > 946684800000) { // Jan 1, 2000
77
+ return numericValue;
78
+ }
79
+ }
72
80
  // Handle natural language dates
73
81
  const lowerDate = dateString.toLowerCase().trim();
74
82
  // Handle "now" specifically
@@ -98,6 +106,44 @@ export function parseDueDate(dateString) {
98
106
  tomorrow.setHours(23, 59, 59, 999);
99
107
  return tomorrow.getTime();
100
108
  }
109
+ // Handle day names (Monday, Tuesday, etc.) - find next occurrence
110
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
111
+ const dayMatch = lowerDate.match(/\b(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/);
112
+ if (dayMatch) {
113
+ const targetDayName = dayMatch[1];
114
+ const targetDayIndex = dayNames.indexOf(targetDayName);
115
+ const today = new Date();
116
+ const currentDayIndex = today.getDay();
117
+ // Calculate days until target day
118
+ let daysUntilTarget = targetDayIndex - currentDayIndex;
119
+ if (daysUntilTarget <= 0) {
120
+ daysUntilTarget += 7; // Next week
121
+ }
122
+ // Handle "next" prefix explicitly
123
+ if (lowerDate.includes('next ')) {
124
+ daysUntilTarget += 7;
125
+ }
126
+ const targetDate = new Date(today);
127
+ targetDate.setDate(today.getDate() + daysUntilTarget);
128
+ // Extract time if specified (e.g., "Friday at 3pm", "Saturday 2:30pm")
129
+ const timeMatch = lowerDate.match(/(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
130
+ if (timeMatch) {
131
+ let hours = parseInt(timeMatch[1]);
132
+ const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0;
133
+ const meridian = timeMatch[3]?.toLowerCase();
134
+ // Convert to 24-hour format
135
+ if (meridian === 'pm' && hours < 12)
136
+ hours += 12;
137
+ if (meridian === 'am' && hours === 12)
138
+ hours = 0;
139
+ targetDate.setHours(hours, minutes, 0, 0);
140
+ }
141
+ else {
142
+ // Default to end of day if no time specified
143
+ targetDate.setHours(23, 59, 59, 999);
144
+ }
145
+ return targetDate.getTime();
146
+ }
101
147
  // Handle relative dates with specific times
102
148
  const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
103
149
  const match = lowerDate.match(relativeTimeRegex);
@@ -191,10 +237,42 @@ export function parseDueDate(dateString) {
191
237
  }
192
238
  return date.getTime();
193
239
  }
194
- // Try to parse as a date string
195
- const date = new Date(dateString);
196
- if (!isNaN(date.getTime())) {
197
- return date.getTime();
240
+ // Enhanced fallback: Try JavaScript's native Date constructor with various formats
241
+ // This handles many natural language formats like "Saturday at 3pm EST", "next Friday", etc.
242
+ const nativeDate = new Date(dateString);
243
+ if (!isNaN(nativeDate.getTime())) {
244
+ // Check if the parsed date is reasonable (not too far in the past or future)
245
+ const now = Date.now();
246
+ const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
247
+ const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
248
+ if (nativeDate.getTime() > oneYearAgo && nativeDate.getTime() < tenYearsFromNow) {
249
+ return nativeDate.getTime();
250
+ }
251
+ }
252
+ // Try some common variations and transformations
253
+ const variations = [
254
+ dateString.replace(/\s+at\s+/i, ' '), // "Saturday at 3pm" -> "Saturday 3pm"
255
+ dateString.replace(/\s+EST|EDT|PST|PDT|CST|CDT|MST|MDT/i, ''), // Remove timezone
256
+ dateString.replace(/next\s+/i, ''), // "next Friday" -> "Friday"
257
+ dateString.replace(/this\s+/i, ''), // "this Friday" -> "Friday"
258
+ ];
259
+ for (const variation of variations) {
260
+ const varDate = new Date(variation);
261
+ if (!isNaN(varDate.getTime())) {
262
+ const now = Date.now();
263
+ const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
264
+ const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
265
+ if (varDate.getTime() > oneYearAgo && varDate.getTime() < tenYearsFromNow) {
266
+ // If the parsed date is in the past, assume they meant next occurrence
267
+ if (varDate.getTime() < now) {
268
+ // Add 7 days if it's a day of the week
269
+ if (dateString.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) {
270
+ varDate.setDate(varDate.getDate() + 7);
271
+ }
272
+ }
273
+ return varDate.getTime();
274
+ }
275
+ }
198
276
  }
199
277
  // If all parsing fails, return undefined
200
278
  return undefined;
@@ -232,30 +310,6 @@ export function formatDueDate(timestamp) {
232
310
  throw new Error(`Invalid timestamp: ${timestamp}`);
233
311
  }
234
312
  }
235
- /**
236
- * Checks if a timestamp is for today
237
- *
238
- * @param timestamp Unix timestamp in milliseconds
239
- * @returns Boolean indicating if the timestamp is for today
240
- */
241
- export function isToday(timestamp) {
242
- const date = new Date(timestamp);
243
- const today = new Date();
244
- return date.getDate() === today.getDate() &&
245
- date.getMonth() === today.getMonth() &&
246
- date.getFullYear() === today.getFullYear();
247
- }
248
- /**
249
- * Get timestamp range for today (start to end)
250
- *
251
- * @returns Object with start and end timestamps for today
252
- */
253
- export function getTodayRange() {
254
- return {
255
- start: getStartOfDay(),
256
- end: getEndOfDay()
257
- };
258
- }
259
313
  /**
260
314
  * Format a date for display in errors and messages
261
315
  * @param timestamp The timestamp to format
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -1,13 +0,0 @@
1
- // src/schemas/member.ts
2
- import { z } from 'zod';
3
- export const MemberSchema = z.object({
4
- id: z.number(),
5
- username: z.string().optional(),
6
- email: z.string().optional(),
7
- full_name: z.string().optional(),
8
- profile_picture: z.string().optional(),
9
- role: z.number(),
10
- role_name: z.string().optional(),
11
- initials: z.string().optional(),
12
- last_active: z.string().optional(),
13
- });
package/build/server.log DELETED
@@ -1 +0,0 @@
1
- Logging initialized to /Volumes/Code/Projects/MCP/clickup-mcp-server/build/server.log