@taazkareem/clickup-mcp-server 0.8.3 → 0.8.5
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 +31 -1
- package/build/config.js +30 -0
- package/build/middleware/security.js +231 -0
- package/build/server.js +1 -1
- package/build/services/clickup/task/task-core.js +69 -5
- package/build/sse_server.js +172 -8
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +7 -7
- package/build/tools/task/handlers.js +66 -15
- package/build/tools/task/single-operations.js +11 -11
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/package.json +1 -1
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.
|
|
9
|
+
> 🚀 **Status Update:** v0.8.5 is released with comprehensive natural language date parsing and critical bug fixes! Added 47+ natural language patterns (100% accuracy), extended time units (months/years), dynamic number support, fixed task assignment functionality, and resolved time tracking issues. See [Release Notes](release-notes.md) for full details.
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
@@ -138,6 +138,36 @@ Available configuration options:
|
|
|
138
138
|
| `ENABLE_SSE` | Enable the HTTP/SSE transport | `false` |
|
|
139
139
|
| `PORT` | Port for the HTTP server | `3231` |
|
|
140
140
|
| `ENABLE_STDIO` | Enable the STDIO transport | `true` |
|
|
141
|
+
| `ENABLE_SECURITY_FEATURES` | Enable security headers and logging | `false` |
|
|
142
|
+
| `ENABLE_HTTPS` | Enable HTTPS/TLS encryption | `false` |
|
|
143
|
+
| `ENABLE_ORIGIN_VALIDATION` | Validate Origin header against whitelist | `false` |
|
|
144
|
+
| `ENABLE_RATE_LIMIT` | Enable rate limiting protection | `false` |
|
|
145
|
+
|
|
146
|
+
### 🔒 Security Features
|
|
147
|
+
|
|
148
|
+
The server includes optional security enhancements for production deployments. All security features are **opt-in** and **disabled by default** to maintain backwards compatibility.
|
|
149
|
+
|
|
150
|
+
**Quick security setup:**
|
|
151
|
+
```bash
|
|
152
|
+
# Generate SSL certificates for HTTPS
|
|
153
|
+
./scripts/generate-ssl-cert.sh
|
|
154
|
+
|
|
155
|
+
# Start with full security
|
|
156
|
+
ENABLE_SECURITY_FEATURES=true \
|
|
157
|
+
ENABLE_HTTPS=true \
|
|
158
|
+
ENABLE_ORIGIN_VALIDATION=true \
|
|
159
|
+
ENABLE_RATE_LIMIT=true \
|
|
160
|
+
SSL_KEY_PATH=./ssl/server.key \
|
|
161
|
+
SSL_CERT_PATH=./ssl/server.crt \
|
|
162
|
+
npx @taazkareem/clickup-mcp-server@latest --env CLICKUP_API_KEY=your-key --env CLICKUP_TEAM_ID=your-team --env ENABLE_SSE=true
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**HTTPS Endpoints:**
|
|
166
|
+
- **Primary**: `https://127.0.0.1:3443/mcp` (Streamable HTTPS)
|
|
167
|
+
- **Legacy**: `https://127.0.0.1:3443/sse` (SSE HTTPS for backwards compatibility)
|
|
168
|
+
- **Health**: `https://127.0.0.1:3443/health` (Health check)
|
|
169
|
+
|
|
170
|
+
For detailed security configuration, see [Security Features Documentation](docs/security-features.md).
|
|
141
171
|
|
|
142
172
|
#### n8n Integration
|
|
143
173
|
|
package/build/config.js
CHANGED
|
@@ -87,6 +87,12 @@ const parseInteger = (value, defaultValue) => {
|
|
|
87
87
|
const parsed = parseInt(value, 10);
|
|
88
88
|
return isNaN(parsed) ? defaultValue : parsed;
|
|
89
89
|
};
|
|
90
|
+
// Parse comma-separated origins list
|
|
91
|
+
const parseOrigins = (value, defaultValue) => {
|
|
92
|
+
if (!value)
|
|
93
|
+
return defaultValue;
|
|
94
|
+
return value.split(',').map(origin => origin.trim()).filter(origin => origin !== '');
|
|
95
|
+
};
|
|
90
96
|
// Load configuration from command line args or environment variables
|
|
91
97
|
const configuration = {
|
|
92
98
|
clickupApiKey: envArgs.clickupApiKey || process.env.CLICKUP_API_KEY || '',
|
|
@@ -100,6 +106,30 @@ const configuration = {
|
|
|
100
106
|
ssePort: parseInteger(envArgs.ssePort || process.env.SSE_PORT, 3000),
|
|
101
107
|
enableStdio: parseBoolean(envArgs.enableStdio || process.env.ENABLE_STDIO, true),
|
|
102
108
|
port: envArgs.port || process.env.PORT || '3231',
|
|
109
|
+
// Security configuration (opt-in for backwards compatibility)
|
|
110
|
+
enableSecurityFeatures: parseBoolean(process.env.ENABLE_SECURITY_FEATURES, false),
|
|
111
|
+
enableOriginValidation: parseBoolean(process.env.ENABLE_ORIGIN_VALIDATION, false),
|
|
112
|
+
enableRateLimit: parseBoolean(process.env.ENABLE_RATE_LIMIT, false),
|
|
113
|
+
enableCors: parseBoolean(process.env.ENABLE_CORS, false),
|
|
114
|
+
allowedOrigins: parseOrigins(process.env.ALLOWED_ORIGINS, [
|
|
115
|
+
'http://127.0.0.1:3231',
|
|
116
|
+
'http://localhost:3231',
|
|
117
|
+
'http://127.0.0.1:3000',
|
|
118
|
+
'http://localhost:3000',
|
|
119
|
+
'https://127.0.0.1:3443',
|
|
120
|
+
'https://localhost:3443',
|
|
121
|
+
'https://127.0.0.1:3231',
|
|
122
|
+
'https://localhost:3231'
|
|
123
|
+
]),
|
|
124
|
+
rateLimitMax: parseInteger(process.env.RATE_LIMIT_MAX, 100),
|
|
125
|
+
rateLimitWindowMs: parseInteger(process.env.RATE_LIMIT_WINDOW_MS, 60000),
|
|
126
|
+
maxRequestSize: process.env.MAX_REQUEST_SIZE || '10mb',
|
|
127
|
+
// HTTPS configuration
|
|
128
|
+
enableHttps: parseBoolean(process.env.ENABLE_HTTPS, false),
|
|
129
|
+
httpsPort: process.env.HTTPS_PORT || '3443',
|
|
130
|
+
sslKeyPath: process.env.SSL_KEY_PATH,
|
|
131
|
+
sslCertPath: process.env.SSL_CERT_PATH,
|
|
132
|
+
sslCaPath: process.env.SSL_CA_PATH,
|
|
103
133
|
};
|
|
104
134
|
// Don't log to console as it interferes with JSON-RPC communication
|
|
105
135
|
// Validate only the required variables are present
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Security Middleware for ClickUp MCP Server
|
|
6
|
+
*
|
|
7
|
+
* This module provides optional security enhancements that can be enabled
|
|
8
|
+
* without breaking existing functionality. All security features are opt-in
|
|
9
|
+
* to maintain backwards compatibility.
|
|
10
|
+
*/
|
|
11
|
+
import rateLimit from 'express-rate-limit';
|
|
12
|
+
import cors from 'cors';
|
|
13
|
+
import config from '../config.js';
|
|
14
|
+
import { Logger } from '../logger.js';
|
|
15
|
+
const logger = new Logger('Security');
|
|
16
|
+
/**
|
|
17
|
+
* Origin validation middleware - validates Origin header against whitelist
|
|
18
|
+
* Only enabled when ENABLE_ORIGIN_VALIDATION=true
|
|
19
|
+
*/
|
|
20
|
+
export function createOriginValidationMiddleware() {
|
|
21
|
+
return (req, res, next) => {
|
|
22
|
+
if (!config.enableOriginValidation) {
|
|
23
|
+
next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const origin = req.headers.origin;
|
|
27
|
+
const referer = req.headers.referer;
|
|
28
|
+
// For non-browser requests (like n8n, MCP Inspector), origin might be undefined
|
|
29
|
+
// In such cases, we allow the request but log it for monitoring
|
|
30
|
+
if (!origin && !referer) {
|
|
31
|
+
logger.debug('Request without Origin/Referer header - allowing (likely non-browser client)', {
|
|
32
|
+
userAgent: req.headers['user-agent'],
|
|
33
|
+
ip: req.ip,
|
|
34
|
+
path: req.path
|
|
35
|
+
});
|
|
36
|
+
next();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Check if origin is in allowed list
|
|
40
|
+
if (origin && !config.allowedOrigins.includes(origin)) {
|
|
41
|
+
logger.warn('Blocked request from unauthorized origin', {
|
|
42
|
+
origin,
|
|
43
|
+
ip: req.ip,
|
|
44
|
+
path: req.path,
|
|
45
|
+
userAgent: req.headers['user-agent']
|
|
46
|
+
});
|
|
47
|
+
res.status(403).json({
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
error: {
|
|
50
|
+
code: -32000,
|
|
51
|
+
message: 'Forbidden: Origin not allowed'
|
|
52
|
+
},
|
|
53
|
+
id: null
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// If referer is present, validate it too
|
|
58
|
+
if (referer) {
|
|
59
|
+
try {
|
|
60
|
+
const refererOrigin = new URL(referer).origin;
|
|
61
|
+
if (!config.allowedOrigins.includes(refererOrigin)) {
|
|
62
|
+
logger.warn('Blocked request from unauthorized referer', {
|
|
63
|
+
referer,
|
|
64
|
+
refererOrigin,
|
|
65
|
+
ip: req.ip,
|
|
66
|
+
path: req.path
|
|
67
|
+
});
|
|
68
|
+
res.status(403).json({
|
|
69
|
+
jsonrpc: '2.0',
|
|
70
|
+
error: {
|
|
71
|
+
code: -32000,
|
|
72
|
+
message: 'Forbidden: Referer not allowed'
|
|
73
|
+
},
|
|
74
|
+
id: null
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.warn('Invalid referer URL', { referer, error: error.message });
|
|
81
|
+
// Continue processing if referer is malformed
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
logger.debug('Origin validation passed', { origin, referer });
|
|
85
|
+
next();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Rate limiting middleware - protects against DoS attacks
|
|
90
|
+
* Only enabled when ENABLE_RATE_LIMIT=true
|
|
91
|
+
*/
|
|
92
|
+
export function createRateLimitMiddleware() {
|
|
93
|
+
if (!config.enableRateLimit) {
|
|
94
|
+
return (_req, _res, next) => next();
|
|
95
|
+
}
|
|
96
|
+
return rateLimit({
|
|
97
|
+
windowMs: config.rateLimitWindowMs,
|
|
98
|
+
max: config.rateLimitMax,
|
|
99
|
+
message: {
|
|
100
|
+
jsonrpc: '2.0',
|
|
101
|
+
error: {
|
|
102
|
+
code: -32000,
|
|
103
|
+
message: 'Too many requests, please try again later'
|
|
104
|
+
},
|
|
105
|
+
id: null
|
|
106
|
+
},
|
|
107
|
+
standardHeaders: true,
|
|
108
|
+
legacyHeaders: false,
|
|
109
|
+
handler: (req, res) => {
|
|
110
|
+
logger.warn('Rate limit exceeded', {
|
|
111
|
+
ip: req.ip,
|
|
112
|
+
path: req.path,
|
|
113
|
+
userAgent: req.headers['user-agent']
|
|
114
|
+
});
|
|
115
|
+
res.status(429).json({
|
|
116
|
+
jsonrpc: '2.0',
|
|
117
|
+
error: {
|
|
118
|
+
code: -32000,
|
|
119
|
+
message: 'Too many requests, please try again later'
|
|
120
|
+
},
|
|
121
|
+
id: null
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* CORS middleware - configures cross-origin resource sharing
|
|
128
|
+
* Only enabled when ENABLE_CORS=true
|
|
129
|
+
*/
|
|
130
|
+
export function createCorsMiddleware() {
|
|
131
|
+
if (!config.enableCors) {
|
|
132
|
+
return (_req, _res, next) => next();
|
|
133
|
+
}
|
|
134
|
+
return cors({
|
|
135
|
+
origin: (origin, callback) => {
|
|
136
|
+
// Allow requests with no origin (like mobile apps, Postman, etc.)
|
|
137
|
+
if (!origin)
|
|
138
|
+
return callback(null, true);
|
|
139
|
+
if (config.allowedOrigins.includes(origin)) {
|
|
140
|
+
callback(null, true);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
logger.warn('CORS blocked origin', { origin });
|
|
144
|
+
callback(new Error('Not allowed by CORS'));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
credentials: true,
|
|
148
|
+
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
149
|
+
allowedHeaders: ['Content-Type', 'mcp-session-id', 'Authorization'],
|
|
150
|
+
exposedHeaders: ['mcp-session-id']
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Security headers middleware - adds security-related HTTP headers
|
|
155
|
+
* Only enabled when ENABLE_SECURITY_FEATURES=true
|
|
156
|
+
*/
|
|
157
|
+
export function createSecurityHeadersMiddleware() {
|
|
158
|
+
return (req, res, next) => {
|
|
159
|
+
if (!config.enableSecurityFeatures) {
|
|
160
|
+
return next();
|
|
161
|
+
}
|
|
162
|
+
// Add security headers
|
|
163
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
164
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
165
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
166
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
167
|
+
// Only add HSTS for HTTPS
|
|
168
|
+
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
|
|
169
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
170
|
+
}
|
|
171
|
+
logger.debug('Security headers applied');
|
|
172
|
+
next();
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Request logging middleware for security monitoring
|
|
177
|
+
*/
|
|
178
|
+
export function createSecurityLoggingMiddleware() {
|
|
179
|
+
return (req, res, next) => {
|
|
180
|
+
if (!config.enableSecurityFeatures) {
|
|
181
|
+
return next();
|
|
182
|
+
}
|
|
183
|
+
const startTime = Date.now();
|
|
184
|
+
res.on('finish', () => {
|
|
185
|
+
const duration = Date.now() - startTime;
|
|
186
|
+
const logData = {
|
|
187
|
+
method: req.method,
|
|
188
|
+
path: req.path,
|
|
189
|
+
statusCode: res.statusCode,
|
|
190
|
+
duration,
|
|
191
|
+
ip: req.ip,
|
|
192
|
+
userAgent: req.headers['user-agent'],
|
|
193
|
+
origin: req.headers.origin,
|
|
194
|
+
sessionId: req.headers['mcp-session-id']
|
|
195
|
+
};
|
|
196
|
+
if (res.statusCode >= 400) {
|
|
197
|
+
logger.warn('HTTP error response', logData);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
logger.debug('HTTP request completed', logData);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
next();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Input validation middleware - validates request size and content
|
|
208
|
+
*/
|
|
209
|
+
export function createInputValidationMiddleware() {
|
|
210
|
+
return (req, res, next) => {
|
|
211
|
+
// Always enforce reasonable request size limits
|
|
212
|
+
const contentLength = req.headers['content-length'];
|
|
213
|
+
if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { // 50MB hard limit
|
|
214
|
+
logger.warn('Request too large', {
|
|
215
|
+
contentLength,
|
|
216
|
+
ip: req.ip,
|
|
217
|
+
path: req.path
|
|
218
|
+
});
|
|
219
|
+
res.status(413).json({
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
error: {
|
|
222
|
+
code: -32000,
|
|
223
|
+
message: 'Request entity too large'
|
|
224
|
+
},
|
|
225
|
+
id: null
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
230
|
+
};
|
|
231
|
+
}
|
package/build/server.js
CHANGED
|
@@ -179,11 +179,29 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
181
|
* Get a task by its ID
|
|
182
|
-
*
|
|
182
|
+
* Automatically detects custom task IDs and routes them appropriately
|
|
183
|
+
* @param taskId The ID of the task to retrieve (regular or custom)
|
|
183
184
|
* @returns The task
|
|
184
185
|
*/
|
|
185
186
|
async getTask(taskId) {
|
|
186
187
|
this.logOperation('getTask', { taskId });
|
|
188
|
+
// Import the detection function here to avoid circular dependencies
|
|
189
|
+
const { isCustomTaskId } = await import('../../../tools/task/utilities.js');
|
|
190
|
+
// Test the detection function
|
|
191
|
+
const isCustom = isCustomTaskId(taskId);
|
|
192
|
+
this.logger.debug('Custom task ID detection result', {
|
|
193
|
+
taskId,
|
|
194
|
+
isCustom,
|
|
195
|
+
taskIdLength: taskId.length,
|
|
196
|
+
containsHyphen: taskId.includes('-'),
|
|
197
|
+
containsUnderscore: taskId.includes('_')
|
|
198
|
+
});
|
|
199
|
+
// Automatically detect custom task IDs and route to appropriate method
|
|
200
|
+
if (isCustom) {
|
|
201
|
+
this.logger.debug('Detected custom task ID, routing to getTaskByCustomId', { taskId });
|
|
202
|
+
return this.getTaskByCustomId(taskId);
|
|
203
|
+
}
|
|
204
|
+
this.logger.debug('Detected regular task ID, using standard getTask flow', { taskId });
|
|
187
205
|
try {
|
|
188
206
|
return await this.makeRequest(async () => {
|
|
189
207
|
const response = await this.client.get(`/task/${taskId}`);
|
|
@@ -196,6 +214,14 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
196
214
|
});
|
|
197
215
|
}
|
|
198
216
|
catch (error) {
|
|
217
|
+
// If this was detected as a regular task ID but failed, provide helpful error message
|
|
218
|
+
// suggesting it might be a custom ID that wasn't properly detected
|
|
219
|
+
if (error instanceof ClickUpServiceError && error.code === ErrorCode.NOT_FOUND) {
|
|
220
|
+
const { isCustomTaskId } = await import('../../../tools/task/utilities.js');
|
|
221
|
+
if (!isCustomTaskId(taskId) && (taskId.includes('-') || taskId.includes('_'))) {
|
|
222
|
+
throw new ClickUpServiceError(`Task ${taskId} not found. If this is a custom task ID, ensure your workspace has custom task IDs enabled and you have access to the task.`, ErrorCode.NOT_FOUND, error.data);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
199
225
|
throw this.handleError(error, `Failed to get task ${taskId}`);
|
|
200
226
|
}
|
|
201
227
|
}
|
|
@@ -232,7 +258,7 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
232
258
|
this.logOperation('getSubtasks', { taskId });
|
|
233
259
|
try {
|
|
234
260
|
return await this.makeRequest(async () => {
|
|
235
|
-
const response = await this.client.get(`/task/${taskId}`);
|
|
261
|
+
const response = await this.client.get(`/task/${taskId}?subtasks=true&include_subtasks=true`);
|
|
236
262
|
// Return subtasks if present, otherwise empty array
|
|
237
263
|
return response.data.subtasks || [];
|
|
238
264
|
});
|
|
@@ -259,6 +285,14 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
259
285
|
custom_task_ids: 'true',
|
|
260
286
|
team_id: this.teamId // team_id is required when custom_task_ids is true
|
|
261
287
|
});
|
|
288
|
+
// Debug logging for troubleshooting
|
|
289
|
+
this.logger.debug('Making custom task ID API request', {
|
|
290
|
+
customTaskId,
|
|
291
|
+
url,
|
|
292
|
+
teamId: this.teamId,
|
|
293
|
+
params: params.toString(),
|
|
294
|
+
fullUrl: `${url}?${params.toString()}`
|
|
295
|
+
});
|
|
262
296
|
// Note: The ClickUp API documentation for GET /task/{task_id} doesn't explicitly mention
|
|
263
297
|
// filtering by list_id when custom_task_ids=true. This parameter might be ignored.
|
|
264
298
|
if (listId) {
|
|
@@ -276,6 +310,13 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
276
310
|
});
|
|
277
311
|
}
|
|
278
312
|
catch (error) {
|
|
313
|
+
// Enhanced error logging for debugging
|
|
314
|
+
this.logger.error('Custom task ID request failed', {
|
|
315
|
+
customTaskId,
|
|
316
|
+
teamId: this.teamId,
|
|
317
|
+
error: error instanceof Error ? error.message : String(error),
|
|
318
|
+
errorDetails: error
|
|
319
|
+
});
|
|
279
320
|
// Provide more specific error context if possible
|
|
280
321
|
if (error instanceof ClickUpServiceError && error.code === ErrorCode.NOT_FOUND) {
|
|
281
322
|
throw new ClickUpServiceError(`Task with custom ID ${customTaskId} not found or not accessible for team ${this.teamId}.`, ErrorCode.NOT_FOUND, error.data);
|
|
@@ -292,11 +333,34 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
292
333
|
async updateTask(taskId, updateData) {
|
|
293
334
|
this.logOperation('updateTask', { taskId, ...updateData });
|
|
294
335
|
try {
|
|
295
|
-
// Extract custom fields from updateData
|
|
296
|
-
const { custom_fields, ...standardFields } = updateData;
|
|
336
|
+
// Extract custom fields and assignees from updateData
|
|
337
|
+
const { custom_fields, assignees, ...standardFields } = updateData;
|
|
338
|
+
// Prepare the fields to send to API
|
|
339
|
+
let fieldsToSend = { ...standardFields };
|
|
340
|
+
// Handle assignees separately if provided
|
|
341
|
+
if (assignees !== undefined) {
|
|
342
|
+
// Get current task to compare assignees
|
|
343
|
+
const currentTask = await this.getTask(taskId);
|
|
344
|
+
const currentAssigneeIds = currentTask.assignees.map(a => a.id);
|
|
345
|
+
let assigneesToProcess;
|
|
346
|
+
if (Array.isArray(assignees)) {
|
|
347
|
+
// If assignees is an array, calculate add/rem based on current vs new
|
|
348
|
+
const newAssigneeIds = assignees;
|
|
349
|
+
assigneesToProcess = {
|
|
350
|
+
add: newAssigneeIds.filter(id => !currentAssigneeIds.includes(id)),
|
|
351
|
+
rem: currentAssigneeIds.filter(id => !newAssigneeIds.includes(id))
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// If assignees is already in add/rem format, use it directly
|
|
356
|
+
assigneesToProcess = assignees;
|
|
357
|
+
}
|
|
358
|
+
// Add assignees to the fields in the correct format
|
|
359
|
+
fieldsToSend.assignees = assigneesToProcess;
|
|
360
|
+
}
|
|
297
361
|
// First update the standard fields
|
|
298
362
|
const updatedTask = await this.makeRequest(async () => {
|
|
299
|
-
const response = await this.client.put(`/task/${taskId}`,
|
|
363
|
+
const response = await this.client.put(`/task/${taskId}`, fieldsToSend);
|
|
300
364
|
// Handle both JSON and text responses
|
|
301
365
|
const data = response.data;
|
|
302
366
|
if (typeof data === 'string') {
|
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
|
}
|