@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/README.md +123 -55
- package/build/config.js +38 -7
- package/build/logger.js +2 -26
- package/build/middleware/security.js +231 -0
- package/build/server.js +32 -6
- package/build/services/clickup/task/task-core.js +1 -1
- package/build/services/clickup/task/task-search.js +163 -0
- package/build/sse_server.js +172 -8
- package/build/tools/documents.js +0 -9
- package/build/tools/task/bulk-operations.js +2 -2
- package/build/tools/task/handlers.js +142 -7
- package/build/tools/task/single-operations.js +2 -2
- package/build/tools/task/workspace-operations.js +1 -0
- package/build/utils/color-processor.js +3 -3
- package/build/utils/date-utils.js +3 -27
- package/package.json +1 -1
- package/build/schemas/member.js +0 -13
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.
|
|
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 =>
|
|
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
|
|
116
|
-
if (
|
|
117
|
-
|
|
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:
|
|
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
|
package/build/sse_server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
}
|
package/build/tools/documents.js
CHANGED
|
@@ -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: "
|
|
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
|
-
//
|
|
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
|
-
//
|
|
600
|
-
|
|
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);
|