@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 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.3 is released with major enhancements! Enhanced workspace tasks filtering with Views API support for multi-list tasks (Issue #43), added ENABLED_TOOLS configuration option (Issue #50), and fixed automatic priority assignment in task creation. See [Release Notes](release-notes.md) for full details.
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
@@ -45,7 +45,7 @@ const isToolEnabled = (toolName) => {
45
45
  };
46
46
  export const server = new Server({
47
47
  name: "clickup-mcp-server",
48
- version: "0.8.3",
48
+ version: "0.8.5",
49
49
  }, {
50
50
  capabilities: {
51
51
  tools: {},
@@ -179,11 +179,29 @@ export class TaskServiceCore extends BaseClickUpService {
179
179
  }
180
180
  /**
181
181
  * Get a task by its ID
182
- * @param taskId The ID of the task to retrieve
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}`, standardFields);
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') {
@@ -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
  }