@taazkareem/clickup-mcp-server 0.8.3 → 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 +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 +1 -1
- package/build/sse_server.js +172 -8
- package/build/tools/task/bulk-operations.js +2 -2
- package/build/tools/task/handlers.js +4 -3
- package/build/tools/task/single-operations.js +2 -2
- 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.4 is released with security features and compatibility improvements! Added comprehensive opt-in enhanced security features, fixed Gemini compatibility (Issue #79), and resolved priority handling and subtask retrieval 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
|
@@ -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
|
});
|
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
|
}
|
|
@@ -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;
|
|
@@ -171,9 +171,9 @@ export const updateTaskTool = {
|
|
|
171
171
|
description: "New status. Must be valid for the task's current list."
|
|
172
172
|
},
|
|
173
173
|
priority: {
|
|
174
|
-
type: "
|
|
174
|
+
type: "string",
|
|
175
175
|
nullable: true,
|
|
176
|
-
enum: [1, 2, 3, 4, null],
|
|
176
|
+
enum: ["1", "2", "3", "4", null],
|
|
177
177
|
description: "New priority: 1 (urgent) to 4 (low). Set null to clear priority."
|
|
178
178
|
},
|
|
179
179
|
dueDate: {
|
package/package.json
CHANGED