@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/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
|
|
|
@@ -55,9 +55,45 @@ Or use this npx command:
|
|
|
55
55
|
|
|
56
56
|
**Obs: if you don't pass "DOCUMENT_SUPPORT": "true", the default is false and document support will not be active.**
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
### Tool Filtering
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
You can control which tools are available using two complementary environment variables:
|
|
61
|
+
|
|
62
|
+
#### ENABLED_TOOLS (Recommended)
|
|
63
|
+
Use `ENABLED_TOOLS` to specify exactly which tools should be available:
|
|
64
|
+
```bash
|
|
65
|
+
# Environment variable
|
|
66
|
+
export ENABLED_TOOLS="create_task,get_task,update_task,get_workspace_hierarchy"
|
|
67
|
+
|
|
68
|
+
# Command line argument
|
|
69
|
+
--env ENABLED_TOOLS=create_task,get_task,update_task,get_workspace_hierarchy
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### DISABLED_TOOLS (Legacy)
|
|
73
|
+
Use `DISABLED_TOOLS` to disable specific tools while keeping all others enabled:
|
|
74
|
+
```bash
|
|
75
|
+
# Environment variable
|
|
76
|
+
export DISABLED_TOOLS="delete_task,delete_bulk_tasks"
|
|
77
|
+
|
|
78
|
+
# Command line argument
|
|
79
|
+
--env DISABLED_TOOLS=delete_task,delete_bulk_tasks
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Precedence Rules
|
|
83
|
+
- If `ENABLED_TOOLS` is specified, only those tools will be available (takes precedence over `DISABLED_TOOLS`)
|
|
84
|
+
- If only `DISABLED_TOOLS` is specified, all tools except those listed will be available
|
|
85
|
+
- If neither is specified, all tools are available (default behavior)
|
|
86
|
+
|
|
87
|
+
**Example:**
|
|
88
|
+
```bash
|
|
89
|
+
# Only enable task creation and reading tools
|
|
90
|
+
npx -y @taazkareem/clickup-mcp-server@latest \
|
|
91
|
+
--env CLICKUP_API_KEY=your-api-key \
|
|
92
|
+
--env CLICKUP_TEAM_ID=your-team-id \
|
|
93
|
+
--env ENABLED_TOOLS=create_task,get_task,get_workspace_hierarchy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Please filter tools you don't need if you are having issues with the number of tools or any context limitations.
|
|
61
97
|
|
|
62
98
|
## Running with HTTP Transport Support
|
|
63
99
|
|
|
@@ -97,9 +133,41 @@ Available configuration options:
|
|
|
97
133
|
|
|
98
134
|
| Option | Description | Default |
|
|
99
135
|
| ------ | ----------- | ------- |
|
|
136
|
+
| `ENABLED_TOOLS` | Comma-separated list of tools to enable (takes precedence) | All tools |
|
|
137
|
+
| `DISABLED_TOOLS` | Comma-separated list of tools to disable | None |
|
|
100
138
|
| `ENABLE_SSE` | Enable the HTTP/SSE transport | `false` |
|
|
101
139
|
| `PORT` | Port for the HTTP server | `3231` |
|
|
102
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).
|
|
103
171
|
|
|
104
172
|
#### n8n Integration
|
|
105
173
|
|
|
@@ -142,55 +210,55 @@ npm run sse-client
|
|
|
142
210
|
|
|
143
211
|
| Tool | Description | Required Parameters |
|
|
144
212
|
| ------------------------------------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
145
|
-
| [get_workspace_hierarchy](docs/
|
|
146
|
-
| [create_task](docs/
|
|
147
|
-
| [create_bulk_tasks](docs/
|
|
148
|
-
| [update_task](docs/
|
|
149
|
-
| [update_bulk_tasks](docs/
|
|
150
|
-
| [get_tasks](docs/
|
|
151
|
-
| [get_task](docs/
|
|
152
|
-
| [get_workspace_tasks](docs/
|
|
153
|
-
| [get_task_comments](docs/
|
|
154
|
-
| [create_task_comment](docs/
|
|
155
|
-
| [attach_task_file](docs/
|
|
156
|
-
| [delete_task](docs/
|
|
157
|
-
| [delete_bulk_tasks](docs/
|
|
158
|
-
| [move_task](docs/
|
|
159
|
-
| [move_bulk_tasks](docs/
|
|
160
|
-
| [duplicate_task](docs/
|
|
161
|
-
| [create_list](docs/
|
|
162
|
-
| [create_folder](docs/
|
|
163
|
-
| [create_list_in_folder](docs/
|
|
164
|
-
| [get_folder](docs/
|
|
165
|
-
| [update_folder](docs/
|
|
166
|
-
| [delete_folder](docs/
|
|
167
|
-
| [get_list](docs/
|
|
168
|
-
| [update_list](docs/
|
|
169
|
-
| [delete_list](docs/
|
|
170
|
-
| [get_space_tags](docs/
|
|
171
|
-
| [create_space_tag](docs/
|
|
172
|
-
| [update_space_tag](docs/
|
|
173
|
-
| [delete_space_tag](docs/
|
|
174
|
-
| [add_tag_to_task](docs/
|
|
175
|
-
| [remove_tag_from_task](docs/
|
|
176
|
-
| [get_task_time_entries](docs/
|
|
177
|
-
| [start_time_tracking](docs/
|
|
178
|
-
| [stop_time_tracking](docs/
|
|
179
|
-
| [add_time_entry](docs/
|
|
180
|
-
| [delete_time_entry](docs/
|
|
181
|
-
| [get_current_time_entry](docs/
|
|
182
|
-
| [get_workspace_members](docs/
|
|
183
|
-
| [find_member_by_name](docs/
|
|
184
|
-
| [resolve_assignees](docs/
|
|
185
|
-
| [create_document](docs/
|
|
186
|
-
| [get_document](docs/
|
|
187
|
-
| [list_documents](docs/
|
|
188
|
-
| [list_document_pages](docs/
|
|
189
|
-
| [get_document_pages](docs/
|
|
190
|
-
| [create_document_pages](docs/
|
|
191
|
-
| [update_document_page](docs/
|
|
192
|
-
|
|
193
|
-
See [full documentation](docs/
|
|
213
|
+
| [get_workspace_hierarchy](docs/user-guide.md#workspace-navigation) | Get workspace structure | None |
|
|
214
|
+
| [create_task](docs/user-guide.md#task-management) | Create a task | `name`, (`listId`/`listName`) |
|
|
215
|
+
| [create_bulk_tasks](docs/user-guide.md#task-management) | Create multiple tasks | `tasks[]` |
|
|
216
|
+
| [update_task](docs/user-guide.md#task-management) | Modify task | `taskId`/`taskName` |
|
|
217
|
+
| [update_bulk_tasks](docs/user-guide.md#task-management) | Update multiple tasks | `tasks[]` with IDs or names |
|
|
218
|
+
| [get_tasks](docs/user-guide.md#task-management) | Get tasks from list | `listId`/`listName` |
|
|
219
|
+
| [get_task](docs/user-guide.md#task-management) | Get single task details | `taskId`/`taskName` (with smart disambiguation) |
|
|
220
|
+
| [get_workspace_tasks](docs/user-guide.md#task-management) | Get tasks with filtering | At least one filter (tags, list_ids, space_ids, etc.) |
|
|
221
|
+
| [get_task_comments](docs/user-guide.md#task-management) | Get comments on a task | `taskId`/`taskName` |
|
|
222
|
+
| [create_task_comment](docs/user-guide.md#task-management) | Add a comment to a task | `commentText`, (`taskId`/(`taskName`+`listName`)) |
|
|
223
|
+
| [attach_task_file](docs/user-guide.md#task-management) | Attach file to a task | `taskId`/`taskName`, (`file_data` or `file_url`) |
|
|
224
|
+
| [delete_task](docs/user-guide.md#task-management) | Remove task | `taskId`/`taskName` |
|
|
225
|
+
| [delete_bulk_tasks](docs/user-guide.md#task-management) | Remove multiple tasks | `tasks[]` with IDs or names |
|
|
226
|
+
| [move_task](docs/user-guide.md#task-management) | Move task | `taskId`/`taskName`, `listId`/`listName` |
|
|
227
|
+
| [move_bulk_tasks](docs/user-guide.md#task-management) | Move multiple tasks | `tasks[]` with IDs or names, target list |
|
|
228
|
+
| [duplicate_task](docs/user-guide.md#task-management) | Copy task | `taskId`/`taskName`, `listId`/`listName` |
|
|
229
|
+
| [create_list](docs/user-guide.md#list-management) | Create list in space | `name`, `spaceId`/`spaceName` |
|
|
230
|
+
| [create_folder](docs/user-guide.md#folder-management) | Create folder | `name`, `spaceId`/`spaceName` |
|
|
231
|
+
| [create_list_in_folder](docs/user-guide.md#list-management) | Create list in folder | `name`, `folderId`/`folderName` |
|
|
232
|
+
| [get_folder](docs/user-guide.md#folder-management) | Get folder details | `folderId`/`folderName` |
|
|
233
|
+
| [update_folder](docs/user-guide.md#folder-management) | Update folder properties | `folderId`/`folderName` |
|
|
234
|
+
| [delete_folder](docs/user-guide.md#folder-management) | Delete folder | `folderId`/`folderName` |
|
|
235
|
+
| [get_list](docs/user-guide.md#list-management) | Get list details | `listId`/`listName` |
|
|
236
|
+
| [update_list](docs/user-guide.md#list-management) | Update list properties | `listId`/`listName` |
|
|
237
|
+
| [delete_list](docs/user-guide.md#list-management) | Delete list | `listId`/`listName` |
|
|
238
|
+
| [get_space_tags](docs/user-guide.md#tag-management) | Get space tags | `spaceId`/`spaceName` |
|
|
239
|
+
| [create_space_tag](docs/user-guide.md#tag-management) | Create tag | `tagName`, `spaceId`/`spaceName` |
|
|
240
|
+
| [update_space_tag](docs/user-guide.md#tag-management) | Update tag | `tagName`, `spaceId`/`spaceName` |
|
|
241
|
+
| [delete_space_tag](docs/user-guide.md#tag-management) | Delete tag | `tagName`, `spaceId`/`spaceName` |
|
|
242
|
+
| [add_tag_to_task](docs/user-guide.md#tag-management) | Add tag to task | `tagName`, `taskId`/(`taskName`+`listName`) |
|
|
243
|
+
| [remove_tag_from_task](docs/user-guide.md#tag-management) | Remove tag from task | `tagName`, `taskId`/(`taskName`+`listName`) |
|
|
244
|
+
| [get_task_time_entries](docs/user-guide.md#time-tracking) | Get time entries for a task | `taskId`/`taskName` |
|
|
245
|
+
| [start_time_tracking](docs/user-guide.md#time-tracking) | Start time tracking on a task | `taskId`/`taskName` |
|
|
246
|
+
| [stop_time_tracking](docs/user-guide.md#time-tracking) | Stop current time tracking | None |
|
|
247
|
+
| [add_time_entry](docs/user-guide.md#time-tracking) | Add manual time entry to a task | `taskId`/`taskName`, `start`, `duration` |
|
|
248
|
+
| [delete_time_entry](docs/user-guide.md#time-tracking) | Delete a time entry | `timeEntryId` |
|
|
249
|
+
| [get_current_time_entry](docs/user-guide.md#time-tracking) | Get currently running timer | None |
|
|
250
|
+
| [get_workspace_members](docs/user-guide.md#member-management) | Get all workspace members | None |
|
|
251
|
+
| [find_member_by_name](docs/user-guide.md#member-management) | Find member by name or email | `nameOrEmail` |
|
|
252
|
+
| [resolve_assignees](docs/user-guide.md#member-management) | Resolve member names to IDs | `assignees[]` |
|
|
253
|
+
| [create_document](docs/user-guide.md#document-management) | Create a document | `workspaceId`, `name`, `parentId`/`parentType`, `visibility`, `create_pages` |
|
|
254
|
+
| [get_document](docs/user-guide.md#document-management) | Get a document | `workspaceId`/`documentId` |
|
|
255
|
+
| [list_documents](docs/user-guide.md#document-management) | List documents | `workspaceId`, `documentId`/`creator`/`deleted`/`archived`/`parent_id`/`parent_type`/`limit`/`next_cursor` |
|
|
256
|
+
| [list_document_pages](docs/user-guide.md#document-management) | List document pages | `documentId`/`documentName` |
|
|
257
|
+
| [get_document_pages](docs/user-guide.md#document-management) | Get document pages | `documentId`/`documentName`, `pageIds` |
|
|
258
|
+
| [create_document_pages](docs/user-guide.md#document-management) | Create a document page | `workspaceId`/`documentId`, `parent_page_id`/`name`/`sub_title`,`content`/`content_format` |
|
|
259
|
+
| [update_document_page](docs/user-guide.md#document-management) | Update a document page | `workspaceId`/`documentId`, `name`/`sub_title`,`content`/`content_edit_mode`/`content_format` |
|
|
260
|
+
|
|
261
|
+
See [full documentation](docs/user-guide.md) for optional parameters and advanced usage.
|
|
194
262
|
|
|
195
263
|
## Member Management Tools
|
|
196
264
|
|
|
@@ -221,9 +289,9 @@ Not yet implemented and not supported by all client apps. Request a feature for
|
|
|
221
289
|
|
|
222
290
|
| Prompt | Purpose | Features |
|
|
223
291
|
| -------------------------------------------------- | ------------------------- | ----------------------------------------- |
|
|
224
|
-
| [summarize_tasks](docs/
|
|
225
|
-
| [analyze_priorities](docs/
|
|
226
|
-
| [generate_description](docs/
|
|
292
|
+
| [summarize_tasks](docs/user-guide.md#prompts) | Task overview | Status summary, priorities, relationships |
|
|
293
|
+
| [analyze_priorities](docs/user-guide.md#prompts) | Priority optimization | Distribution analysis, sequencing |
|
|
294
|
+
| [generate_description](docs/user-guide.md#prompts) | Task description creation | Objectives, criteria, dependencies |
|
|
227
295
|
|
|
228
296
|
## Error Handling
|
|
229
297
|
|
package/build/config.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* The default value is 'false' (string), which means document support will be disabled if
|
|
13
13
|
* no parameter is passed. Pass it as 'true' (string) to enable it.
|
|
14
14
|
*
|
|
15
|
+
* Tool filtering options:
|
|
16
|
+
* - ENABLED_TOOLS: Comma-separated list of tools to enable (takes precedence over DISABLED_TOOLS)
|
|
17
|
+
* - DISABLED_TOOLS: Comma-separated list of tools to disable (ignored if ENABLED_TOOLS is specified)
|
|
18
|
+
*
|
|
15
19
|
* Server transport options:
|
|
16
20
|
* - ENABLE_SSE: Enable Server-Sent Events transport (default: false)
|
|
17
21
|
* - SSE_PORT: Port for SSE server (default: 3000)
|
|
@@ -29,16 +33,12 @@ for (let i = 0; i < args.length; i++) {
|
|
|
29
33
|
envArgs.clickupTeamId = value;
|
|
30
34
|
if (key === 'DOCUMENT_SUPPORT')
|
|
31
35
|
envArgs.documentSupport = value;
|
|
32
|
-
if (key === 'DOCUMENT_MODEL')
|
|
33
|
-
envArgs.documentSupport = value; // Backward compatibility
|
|
34
|
-
if (key === 'DOCUMENT_MODULE')
|
|
35
|
-
envArgs.documentSupport = value; // Backward compatibility
|
|
36
36
|
if (key === 'LOG_LEVEL')
|
|
37
37
|
envArgs.logLevel = value;
|
|
38
38
|
if (key === 'DISABLED_TOOLS')
|
|
39
39
|
envArgs.disabledTools = value;
|
|
40
|
-
if (key === '
|
|
41
|
-
envArgs.
|
|
40
|
+
if (key === 'ENABLED_TOOLS')
|
|
41
|
+
envArgs.enabledTools = value;
|
|
42
42
|
if (key === 'ENABLE_SSE')
|
|
43
43
|
envArgs.enableSSE = value;
|
|
44
44
|
if (key === 'SSE_PORT')
|
|
@@ -60,7 +60,7 @@ export var LogLevel;
|
|
|
60
60
|
LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
|
|
61
61
|
})(LogLevel || (LogLevel = {}));
|
|
62
62
|
// Parse LOG_LEVEL string to LogLevel enum
|
|
63
|
-
|
|
63
|
+
const parseLogLevel = (levelStr) => {
|
|
64
64
|
if (!levelStr)
|
|
65
65
|
return LogLevel.ERROR; // Default to ERROR if not specified
|
|
66
66
|
switch (levelStr.toUpperCase()) {
|
|
@@ -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 || '',
|
|
@@ -95,10 +101,35 @@ const configuration = {
|
|
|
95
101
|
documentSupport: envArgs.documentSupport || process.env.DOCUMENT_SUPPORT || process.env.DOCUMENT_MODULE || process.env.DOCUMENT_MODEL || 'false',
|
|
96
102
|
logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL),
|
|
97
103
|
disabledTools: ((envArgs.disabledTools || process.env.DISABLED_TOOLS || process.env.DISABLED_COMMANDS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
|
|
104
|
+
enabledTools: ((envArgs.enabledTools || process.env.ENABLED_TOOLS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
|
|
98
105
|
enableSSE: parseBoolean(envArgs.enableSSE || process.env.ENABLE_SSE, false),
|
|
99
106
|
ssePort: parseInteger(envArgs.ssePort || process.env.SSE_PORT, 3000),
|
|
100
107
|
enableStdio: parseBoolean(envArgs.enableStdio || process.env.ENABLE_STDIO, true),
|
|
101
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,
|
|
102
133
|
};
|
|
103
134
|
// Don't log to console as it interferes with JSON-RPC communication
|
|
104
135
|
// Validate only the required variables are present
|
package/build/logger.js
CHANGED
|
@@ -29,7 +29,7 @@ export { LogLevel };
|
|
|
29
29
|
* @param level The log level to check
|
|
30
30
|
* @returns True if the level should be logged
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
function isLevelEnabled(level) {
|
|
33
33
|
return level >= configuredLevel;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
@@ -38,7 +38,7 @@ export function isLevelEnabled(level) {
|
|
|
38
38
|
* @param message Message to log
|
|
39
39
|
* @param data Optional data to include in log
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
function log(level, message, data) {
|
|
42
42
|
const levelEnum = level === 'trace' ? LogLevel.TRACE
|
|
43
43
|
: level === 'debug' ? LogLevel.DEBUG
|
|
44
44
|
: level === 'info' ? LogLevel.INFO
|
|
@@ -78,22 +78,6 @@ export function log(level, message, data) {
|
|
|
78
78
|
// Write to file only, not to stderr which would interfere with JSON-RPC
|
|
79
79
|
logStream.write(logMessage + '\n');
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Shorthand for trace level logs
|
|
83
|
-
* @param message Message to log
|
|
84
|
-
* @param data Optional data to include in log
|
|
85
|
-
*/
|
|
86
|
-
export function trace(message, data) {
|
|
87
|
-
log('trace', message, data);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Shorthand for debug level logs
|
|
91
|
-
* @param message Message to log
|
|
92
|
-
* @param data Optional data to include in log
|
|
93
|
-
*/
|
|
94
|
-
export function debug(message, data) {
|
|
95
|
-
log('debug', message, data);
|
|
96
|
-
}
|
|
97
81
|
/**
|
|
98
82
|
* Shorthand for info level logs
|
|
99
83
|
* @param message Message to log
|
|
@@ -102,14 +86,6 @@ export function debug(message, data) {
|
|
|
102
86
|
export function info(message, data) {
|
|
103
87
|
log('info', message, data);
|
|
104
88
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Shorthand for warn level logs
|
|
107
|
-
* @param message Message to log
|
|
108
|
-
* @param data Optional data to include in log
|
|
109
|
-
*/
|
|
110
|
-
export function warn(message, data) {
|
|
111
|
-
log('warn', message, data);
|
|
112
|
-
}
|
|
113
89
|
/**
|
|
114
90
|
* Shorthand for error level logs
|
|
115
91
|
* @param message Message to log
|
|
@@ -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
|
+
}
|