@taazkareem/clickup-mcp-server 0.8.2 → 0.8.4

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/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.2",
48
+ version: "0.8.4",
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 {
@@ -232,7 +232,7 @@ export class TaskServiceCore extends BaseClickUpService {
232
232
  this.logOperation('getSubtasks', { taskId });
233
233
  try {
234
234
  return await this.makeRequest(async () => {
235
- const response = await this.client.get(`/task/${taskId}`);
235
+ const response = await this.client.get(`/task/${taskId}?subtasks=true&include_subtasks=true`);
236
236
  // Return subtasks if present, otherwise empty array
237
237
  return response.data.subtasks || [];
238
238
  });
@@ -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
@@ -12,13 +12,43 @@ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
12
12
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13
13
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
14
14
  import express from 'express';
15
+ import https from 'https';
16
+ import http from 'http';
17
+ import fs from 'fs';
15
18
  import { server, configureServer } from './server.js';
16
19
  import configuration from './config.js';
20
+ import { createOriginValidationMiddleware, createRateLimitMiddleware, createCorsMiddleware, createSecurityHeadersMiddleware, createSecurityLoggingMiddleware, createInputValidationMiddleware } from './middleware/security.js';
21
+ import { Logger } from './logger.js';
17
22
  const app = express();
18
- app.use(express.json());
23
+ const logger = new Logger('SSEServer');
19
24
  export function startSSEServer() {
20
25
  // Configure the unified server first
21
26
  configureServer();
27
+ // Apply security middleware (all are opt-in via environment variables)
28
+ logger.info('Configuring security middleware', {
29
+ securityFeatures: configuration.enableSecurityFeatures,
30
+ originValidation: configuration.enableOriginValidation,
31
+ rateLimit: configuration.enableRateLimit,
32
+ cors: configuration.enableCors
33
+ });
34
+ // Always apply input validation (reasonable defaults)
35
+ app.use(createInputValidationMiddleware());
36
+ // Apply optional security middleware
37
+ app.use(createSecurityLoggingMiddleware());
38
+ app.use(createSecurityHeadersMiddleware());
39
+ app.use(createCorsMiddleware());
40
+ app.use(createOriginValidationMiddleware());
41
+ app.use(createRateLimitMiddleware());
42
+ // Configure JSON parsing with configurable size limit
43
+ app.use(express.json({
44
+ limit: configuration.maxRequestSize,
45
+ verify: (req, res, buf) => {
46
+ // Additional validation can be added here if needed
47
+ if (buf.length === 0) {
48
+ logger.debug('Empty request body received');
49
+ }
50
+ }
51
+ }));
22
52
  const transports = {
23
53
  streamable: {},
24
54
  sse: {},
@@ -27,6 +57,12 @@ export function startSSEServer() {
27
57
  app.post('/mcp', async (req, res) => {
28
58
  try {
29
59
  const sessionId = req.headers['mcp-session-id'];
60
+ logger.debug('MCP request received', {
61
+ sessionId,
62
+ hasBody: !!req.body,
63
+ contentType: req.headers['content-type'],
64
+ origin: req.headers.origin
65
+ });
30
66
  let transport;
31
67
  if (sessionId && transports.streamable[sessionId]) {
32
68
  transport = transports.streamable[sessionId];
@@ -87,7 +123,11 @@ export function startSSEServer() {
87
123
  app.get('/sse', async (req, res) => {
88
124
  const transport = new SSEServerTransport('/messages', res);
89
125
  transports.sse[transport.sessionId] = transport;
90
- console.log(`New SSE connection established with sessionId: ${transport.sessionId}`);
126
+ logger.info('New SSE connection established', {
127
+ sessionId: transport.sessionId,
128
+ origin: req.headers.origin,
129
+ userAgent: req.headers['user-agent']
130
+ });
91
131
  res.on('close', () => {
92
132
  delete transports.sse[transport.sessionId];
93
133
  });
@@ -103,11 +143,135 @@ export function startSSEServer() {
103
143
  res.status(400).send('No transport found for sessionId');
104
144
  }
105
145
  });
106
- const PORT = Number(configuration.port ?? '3231');
107
- // Bind to localhost only for security
108
- app.listen(PORT, () => {
109
- console.log(`Server started on http://127.0.0.1:${PORT}`);
110
- console.log(`Streamable HTTP endpoint: http://127.0.0.1:${PORT}/mcp`);
111
- console.log(`Legacy SSE endpoint: http://127.0.0.1:${PORT}/sse`);
146
+ // Health check endpoint
147
+ app.get('/health', (req, res) => {
148
+ res.json({
149
+ status: 'healthy',
150
+ timestamp: new Date().toISOString(),
151
+ version: '0.8.3',
152
+ security: {
153
+ featuresEnabled: configuration.enableSecurityFeatures,
154
+ originValidation: configuration.enableOriginValidation,
155
+ rateLimit: configuration.enableRateLimit,
156
+ cors: configuration.enableCors
157
+ }
158
+ });
112
159
  });
160
+ // Server creation and startup
161
+ const PORT = Number(configuration.port ?? '3231');
162
+ const HTTPS_PORT = Number(configuration.httpsPort ?? '3443');
163
+ // Function to create and start HTTP server
164
+ function startHttpServer() {
165
+ const httpServer = http.createServer(app);
166
+ httpServer.listen(PORT, '127.0.0.1', () => {
167
+ logger.info('ClickUp MCP Server (HTTP) started', {
168
+ port: PORT,
169
+ protocol: 'http',
170
+ endpoints: {
171
+ streamableHttp: `http://127.0.0.1:${PORT}/mcp`,
172
+ legacySSE: `http://127.0.0.1:${PORT}/sse`,
173
+ health: `http://127.0.0.1:${PORT}/health`
174
+ },
175
+ security: {
176
+ featuresEnabled: configuration.enableSecurityFeatures,
177
+ originValidation: configuration.enableOriginValidation,
178
+ rateLimit: configuration.enableRateLimit,
179
+ cors: configuration.enableCors,
180
+ httpsEnabled: configuration.enableHttps
181
+ }
182
+ });
183
+ console.log(`✅ ClickUp MCP Server started on http://127.0.0.1:${PORT}`);
184
+ console.log(`📡 Streamable HTTP endpoint: http://127.0.0.1:${PORT}/mcp`);
185
+ console.log(`🔄 Legacy SSE endpoint: http://127.0.0.1:${PORT}/sse`);
186
+ console.log(`❤️ Health check: http://127.0.0.1:${PORT}/health`);
187
+ if (configuration.enableHttps) {
188
+ console.log(`⚠️ HTTP server running alongside HTTPS - consider disabling HTTP in production`);
189
+ }
190
+ });
191
+ return httpServer;
192
+ }
193
+ // Function to create and start HTTPS server
194
+ function startHttpsServer() {
195
+ if (!configuration.sslKeyPath || !configuration.sslCertPath) {
196
+ logger.error('HTTPS enabled but SSL certificate paths not provided', {
197
+ sslKeyPath: configuration.sslKeyPath,
198
+ sslCertPath: configuration.sslCertPath
199
+ });
200
+ console.log(`❌ HTTPS enabled but SSL_KEY_PATH and SSL_CERT_PATH not provided`);
201
+ console.log(` Set SSL_KEY_PATH and SSL_CERT_PATH environment variables`);
202
+ return null;
203
+ }
204
+ try {
205
+ // Check if certificate files exist
206
+ if (!fs.existsSync(configuration.sslKeyPath)) {
207
+ throw new Error(`SSL key file not found: ${configuration.sslKeyPath}`);
208
+ }
209
+ if (!fs.existsSync(configuration.sslCertPath)) {
210
+ throw new Error(`SSL certificate file not found: ${configuration.sslCertPath}`);
211
+ }
212
+ const httpsOptions = {
213
+ key: fs.readFileSync(configuration.sslKeyPath),
214
+ cert: fs.readFileSync(configuration.sslCertPath)
215
+ };
216
+ // Add CA certificate if provided
217
+ if (configuration.sslCaPath && fs.existsSync(configuration.sslCaPath)) {
218
+ httpsOptions.ca = fs.readFileSync(configuration.sslCaPath);
219
+ }
220
+ const httpsServer = https.createServer(httpsOptions, app);
221
+ httpsServer.listen(HTTPS_PORT, '127.0.0.1', () => {
222
+ logger.info('ClickUp MCP Server (HTTPS) started', {
223
+ port: HTTPS_PORT,
224
+ protocol: 'https',
225
+ endpoints: {
226
+ streamableHttp: `https://127.0.0.1:${HTTPS_PORT}/mcp`,
227
+ legacySSE: `https://127.0.0.1:${HTTPS_PORT}/sse`,
228
+ health: `https://127.0.0.1:${HTTPS_PORT}/health`
229
+ },
230
+ security: {
231
+ featuresEnabled: configuration.enableSecurityFeatures,
232
+ originValidation: configuration.enableOriginValidation,
233
+ rateLimit: configuration.enableRateLimit,
234
+ cors: configuration.enableCors,
235
+ httpsEnabled: true
236
+ }
237
+ });
238
+ console.log(`🔒 ClickUp MCP Server (HTTPS) started on https://127.0.0.1:${HTTPS_PORT}`);
239
+ console.log(`📡 Streamable HTTPS endpoint: https://127.0.0.1:${HTTPS_PORT}/mcp`);
240
+ console.log(`🔄 Legacy SSE HTTPS endpoint: https://127.0.0.1:${HTTPS_PORT}/sse`);
241
+ console.log(`❤️ Health check HTTPS: https://127.0.0.1:${HTTPS_PORT}/health`);
242
+ });
243
+ return httpsServer;
244
+ }
245
+ catch (error) {
246
+ logger.error('Failed to start HTTPS server', {
247
+ error: error.message,
248
+ sslKeyPath: configuration.sslKeyPath,
249
+ sslCertPath: configuration.sslCertPath
250
+ });
251
+ console.log(`❌ Failed to start HTTPS server: ${error.message}`);
252
+ return null;
253
+ }
254
+ }
255
+ // Start servers based on configuration
256
+ const servers = [];
257
+ // Always start HTTP server (for backwards compatibility)
258
+ servers.push(startHttpServer());
259
+ // Start HTTPS server if enabled
260
+ if (configuration.enableHttps) {
261
+ const httpsServer = startHttpsServer();
262
+ if (httpsServer) {
263
+ servers.push(httpsServer);
264
+ }
265
+ }
266
+ // Security status logging
267
+ if (configuration.enableSecurityFeatures) {
268
+ console.log(`🔒 Security features enabled`);
269
+ }
270
+ else {
271
+ console.log(`⚠️ Security features disabled (set ENABLE_SECURITY_FEATURES=true to enable)`);
272
+ }
273
+ if (!configuration.enableHttps) {
274
+ console.log(`⚠️ HTTPS disabled (set ENABLE_HTTPS=true with SSL certificates to enable)`);
275
+ }
276
+ return servers;
113
277
  }
@@ -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
- ];
@@ -206,9 +206,9 @@ export const updateBulkTasksTool = {
206
206
  description: "New status"
207
207
  },
208
208
  priority: {
209
- type: "number",
209
+ type: "string",
210
210
  nullable: true,
211
- enum: [1, 2, 3, 4, null],
211
+ enum: ["1", "2", "3", "4", null],
212
212
  description: "New priority (1-4 or null)"
213
213
  },
214
214
  dueDate: {
@@ -138,9 +138,10 @@ async function buildUpdateData(params) {
138
138
  updateData.markdown_description = params.markdown_description;
139
139
  if (params.status !== undefined)
140
140
  updateData.status = params.status;
141
- // Skip toTaskPriority conversion since we're handling priority in the main handler
142
- if (params.priority !== undefined)
143
- updateData.priority = params.priority;
141
+ // Use toTaskPriority to properly handle null values and validation
142
+ if (params.priority !== undefined) {
143
+ updateData.priority = toTaskPriority(params.priority);
144
+ }
144
145
  if (params.dueDate !== undefined) {
145
146
  updateData.due_date = parseDueDate(params.dueDate);
146
147
  updateData.due_date_time = true;
@@ -434,13 +435,16 @@ export async function createTaskHandler(params) {
434
435
  description,
435
436
  markdown_description,
436
437
  status,
437
- priority,
438
438
  parent,
439
439
  tags,
440
440
  custom_fields,
441
441
  check_required_custom_fields,
442
442
  assignees: resolvedAssignees
443
443
  };
444
+ // Only include priority if explicitly provided by the user
445
+ if (priority !== undefined) {
446
+ taskData.priority = priority;
447
+ }
444
448
  // Add due date if specified
445
449
  if (dueDate) {
446
450
  taskData.due_date = parseDueDate(dueDate);
@@ -596,8 +600,135 @@ export async function getWorkspaceTasksHandler(taskService, params) {
596
600
  if (!hasFilter) {
597
601
  throw new Error('At least one filter parameter is required (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)');
598
602
  }
599
- // For workspace tasks, we'll continue to use the direct getWorkspaceTasks method
600
- // since it supports specific workspace-wide filters that aren't part of the unified findTasks
603
+ // Check if list_ids are provided for enhanced filtering via Views API
604
+ if (params.list_ids && params.list_ids.length > 0) {
605
+ logger.info('Using Views API for enhanced list filtering', {
606
+ listIds: params.list_ids,
607
+ listCount: params.list_ids.length
608
+ });
609
+ // Warning for broad queries
610
+ const hasOnlyListIds = Object.keys(params).filter(key => params[key] !== undefined && key !== 'list_ids' && key !== 'detail_level').length === 0;
611
+ if (hasOnlyListIds && params.list_ids.length > 5) {
612
+ logger.warn('Broad query detected: many lists with no additional filters', {
613
+ listCount: params.list_ids.length,
614
+ recommendation: 'Consider adding additional filters (tags, statuses, assignees, etc.) for better performance'
615
+ });
616
+ }
617
+ // Use Views API for enhanced list filtering
618
+ let allTasks = [];
619
+ const processedTaskIds = new Set();
620
+ // Create promises for concurrent fetching
621
+ const fetchPromises = params.list_ids.map(async (listId) => {
622
+ try {
623
+ // Get the default list view ID
624
+ const viewId = await taskService.getListViews(listId);
625
+ if (!viewId) {
626
+ logger.warn(`No default view found for list ${listId}, skipping`);
627
+ return [];
628
+ }
629
+ // Extract filters supported by the Views API
630
+ const supportedFilters = {
631
+ subtasks: params.subtasks,
632
+ include_closed: params.include_closed,
633
+ archived: params.archived,
634
+ order_by: params.order_by,
635
+ reverse: params.reverse,
636
+ page: params.page,
637
+ statuses: params.statuses,
638
+ assignees: params.assignees,
639
+ date_created_gt: params.date_created_gt,
640
+ date_created_lt: params.date_created_lt,
641
+ date_updated_gt: params.date_updated_gt,
642
+ date_updated_lt: params.date_updated_lt,
643
+ due_date_gt: params.due_date_gt,
644
+ due_date_lt: params.due_date_lt,
645
+ custom_fields: params.custom_fields
646
+ };
647
+ // Get tasks from the view
648
+ const tasksFromView = await taskService.getTasksFromView(viewId, supportedFilters);
649
+ return tasksFromView;
650
+ }
651
+ catch (error) {
652
+ logger.error(`Failed to get tasks from list ${listId}`, { error: error.message });
653
+ return []; // Continue with other lists even if one fails
654
+ }
655
+ });
656
+ // Execute all fetches concurrently
657
+ const taskArrays = await Promise.all(fetchPromises);
658
+ // Aggregate tasks and remove duplicates
659
+ for (const tasks of taskArrays) {
660
+ for (const task of tasks) {
661
+ if (!processedTaskIds.has(task.id)) {
662
+ allTasks.push(task);
663
+ processedTaskIds.add(task.id);
664
+ }
665
+ }
666
+ }
667
+ logger.info('Aggregated tasks from Views API', {
668
+ totalTasks: allTasks.length,
669
+ uniqueTasks: processedTaskIds.size
670
+ });
671
+ // Apply client-side filtering for unsupported filters
672
+ if (params.tags && params.tags.length > 0) {
673
+ allTasks = allTasks.filter(task => params.tags.every((tag) => task.tags.some(t => t.name === tag)));
674
+ logger.debug('Applied client-side tag filtering', {
675
+ tags: params.tags,
676
+ remainingTasks: allTasks.length
677
+ });
678
+ }
679
+ if (params.folder_ids && params.folder_ids.length > 0) {
680
+ allTasks = allTasks.filter(task => task.folder && params.folder_ids.includes(task.folder.id));
681
+ logger.debug('Applied client-side folder filtering', {
682
+ folderIds: params.folder_ids,
683
+ remainingTasks: allTasks.length
684
+ });
685
+ }
686
+ if (params.space_ids && params.space_ids.length > 0) {
687
+ allTasks = allTasks.filter(task => params.space_ids.includes(task.space.id));
688
+ logger.debug('Applied client-side space filtering', {
689
+ spaceIds: params.space_ids,
690
+ remainingTasks: allTasks.length
691
+ });
692
+ }
693
+ // Check token limit and format response
694
+ const shouldUseSummary = params.detail_level === 'summary' || wouldExceedTokenLimit({ tasks: allTasks });
695
+ if (shouldUseSummary) {
696
+ logger.info('Using summary format for Views API response', {
697
+ totalTasks: allTasks.length,
698
+ reason: params.detail_level === 'summary' ? 'requested' : 'token_limit'
699
+ });
700
+ return {
701
+ summaries: allTasks.map(task => ({
702
+ id: task.id,
703
+ name: task.name,
704
+ status: task.status.status,
705
+ list: {
706
+ id: task.list.id,
707
+ name: task.list.name
708
+ },
709
+ due_date: task.due_date,
710
+ url: task.url,
711
+ priority: task.priority?.priority || null,
712
+ tags: task.tags.map(tag => ({
713
+ name: tag.name,
714
+ tag_bg: tag.tag_bg,
715
+ tag_fg: tag.tag_fg
716
+ }))
717
+ })),
718
+ total_count: allTasks.length,
719
+ has_more: false,
720
+ next_page: 0
721
+ };
722
+ }
723
+ return {
724
+ tasks: allTasks,
725
+ total_count: allTasks.length,
726
+ has_more: false,
727
+ next_page: 0
728
+ };
729
+ }
730
+ // Fallback to existing workspace-wide task retrieval when list_ids are not provided
731
+ logger.info('Using standard workspace task retrieval');
601
732
  const filters = {
602
733
  tags: params.tags,
603
734
  list_ids: params.list_ids,
@@ -664,11 +795,15 @@ export async function createBulkTasksHandler(params) {
664
795
  description: task.description,
665
796
  markdown_description: task.markdown_description,
666
797
  status: task.status,
667
- priority: toTaskPriority(task.priority),
668
798
  tags: task.tags,
669
799
  custom_fields: task.custom_fields,
670
800
  assignees: resolvedAssignees
671
801
  };
802
+ // Only include priority if explicitly provided by the user
803
+ const priority = toTaskPriority(task.priority);
804
+ if (priority !== undefined) {
805
+ taskData.priority = priority;
806
+ }
672
807
  // Add due date if specified
673
808
  if (task.dueDate) {
674
809
  taskData.due_date = parseDueDate(task.dueDate);