@alanse/clickup-multi-mcp-server 1.0.0

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.
Files changed (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -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
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * MCP Server for ClickUp integration
6
+ */
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
+ import config from "./config.js";
10
+ import { workspaceHierarchyTool, handleGetWorkspaceHierarchy } from "./tools/workspace.js";
11
+ import { createTaskTool, updateTaskTool, moveTaskTool, duplicateTaskTool, getTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool, createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool, attachTaskFileTool, getWorkspaceTasksTool, getTaskTimeEntriesTool, startTimeTrackingTool, stopTimeTrackingTool, addTimeEntryTool, deleteTimeEntryTool, getCurrentTimeEntryTool, handleCreateTask, handleUpdateTask, handleMoveTask, handleDuplicateTask, handleDeleteTask, handleGetTaskComments, handleCreateTaskComment, handleCreateBulkTasks, handleUpdateBulkTasks, handleMoveBulkTasks, handleDeleteBulkTasks, handleGetTask, handleAttachTaskFile, handleGetWorkspaceTasks, handleGetTaskTimeEntries, handleStartTimeTracking, handleStopTimeTracking, handleAddTimeEntry, handleDeleteTimeEntry, handleGetCurrentTimeEntry } from "./tools/task/index.js";
12
+ import { createListTool, handleCreateList, createListInFolderTool, handleCreateListInFolder, getListTool, handleGetList, updateListTool, handleUpdateList, deleteListTool, handleDeleteList } from "./tools/list.js";
13
+ import { createFolderTool, handleCreateFolder, getFolderTool, handleGetFolder, updateFolderTool, handleUpdateFolder, deleteFolderTool, handleDeleteFolder } from "./tools/folder.js";
14
+ import { getSpaceTagsTool, handleGetSpaceTags, addTagToTaskTool, handleAddTagToTask, removeTagFromTaskTool, handleRemoveTagFromTask } from "./tools/tag.js";
15
+ import { createDocumentTool, handleCreateDocument, getDocumentTool, handleGetDocument, listDocumentsTool, handleListDocuments, listDocumentPagesTool, handleListDocumentPages, getDocumentPagesTool, handleGetDocumentPages, createDocumentPageTool, handleCreateDocumentPage, updateDocumentPageTool, handleUpdateDocumentPage } from "./tools/documents.js";
16
+ import { getWorkspaceMembersTool, handleGetWorkspaceMembers, findMemberByNameTool, handleFindMemberByName, resolveAssigneesTool, handleResolveAssignees } from "./tools/member.js";
17
+ import { Logger } from "./logger.js";
18
+ import { clickUpServices } from "./services/shared.js";
19
+ import { enhanceToolsWithWorkspace } from "./tools/tool-enhancer.js";
20
+ // Create a logger instance for server
21
+ const logger = new Logger('Server');
22
+ // Use existing services from shared module instead of creating new ones
23
+ const { workspace } = clickUpServices;
24
+ /**
25
+ * Determines if a tool should be enabled based on ENABLED_TOOLS and DISABLED_TOOLS configuration.
26
+ *
27
+ * Logic:
28
+ * 1. If ENABLED_TOOLS is specified, only tools in that list are enabled (ENABLED_TOOLS takes precedence)
29
+ * 2. If ENABLED_TOOLS is not specified but DISABLED_TOOLS is, all tools except those in DISABLED_TOOLS are enabled
30
+ * 3. If neither is specified, all tools are enabled
31
+ *
32
+ * @param toolName - The name of the tool to check
33
+ * @returns true if the tool should be enabled, false otherwise
34
+ */
35
+ const isToolEnabled = (toolName) => {
36
+ // If ENABLED_TOOLS is specified, it takes precedence
37
+ if (config.enabledTools.length > 0) {
38
+ return config.enabledTools.includes(toolName);
39
+ }
40
+ // If only DISABLED_TOOLS is specified, enable all tools except those disabled
41
+ if (config.disabledTools.length > 0) {
42
+ return !config.disabledTools.includes(toolName);
43
+ }
44
+ // If neither is specified, enable all tools
45
+ return true;
46
+ };
47
+ export const server = new Server({
48
+ name: "clickup-mcp-server",
49
+ version: "0.8.5",
50
+ }, {
51
+ capabilities: {
52
+ tools: {},
53
+ prompts: {},
54
+ resources: {},
55
+ },
56
+ });
57
+ const documentModule = () => {
58
+ if (config.documentSupport === 'true') {
59
+ return [
60
+ createDocumentTool,
61
+ getDocumentTool,
62
+ listDocumentsTool,
63
+ listDocumentPagesTool,
64
+ getDocumentPagesTool,
65
+ createDocumentPageTool,
66
+ updateDocumentPageTool,
67
+ ];
68
+ }
69
+ else {
70
+ return [];
71
+ }
72
+ };
73
+ /**
74
+ * Configure the server routes and handlers
75
+ */
76
+ export function configureServer() {
77
+ logger.info("Registering server request handlers");
78
+ // Register ListTools handler
79
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
80
+ logger.debug("Received ListTools request");
81
+ // Collect all tool definitions
82
+ const allTools = [
83
+ workspaceHierarchyTool,
84
+ createTaskTool,
85
+ getTaskTool,
86
+ updateTaskTool,
87
+ moveTaskTool,
88
+ duplicateTaskTool,
89
+ deleteTaskTool,
90
+ getTaskCommentsTool,
91
+ createTaskCommentTool,
92
+ attachTaskFileTool,
93
+ createBulkTasksTool,
94
+ updateBulkTasksTool,
95
+ moveBulkTasksTool,
96
+ deleteBulkTasksTool,
97
+ getWorkspaceTasksTool,
98
+ getTaskTimeEntriesTool,
99
+ startTimeTrackingTool,
100
+ stopTimeTrackingTool,
101
+ addTimeEntryTool,
102
+ deleteTimeEntryTool,
103
+ getCurrentTimeEntryTool,
104
+ createListTool,
105
+ createListInFolderTool,
106
+ getListTool,
107
+ updateListTool,
108
+ deleteListTool,
109
+ createFolderTool,
110
+ getFolderTool,
111
+ updateFolderTool,
112
+ deleteFolderTool,
113
+ getSpaceTagsTool,
114
+ addTagToTaskTool,
115
+ removeTagFromTaskTool,
116
+ getWorkspaceMembersTool,
117
+ findMemberByNameTool,
118
+ resolveAssigneesTool,
119
+ ...documentModule()
120
+ ];
121
+ // Enhance all tools with workspace parameter support
122
+ const enhancedTools = enhanceToolsWithWorkspace(allTools);
123
+ // Filter based on enabled/disabled tools configuration
124
+ return {
125
+ tools: enhancedTools.filter(tool => isToolEnabled(tool.name))
126
+ };
127
+ });
128
+ // Add handler for resources/list
129
+ server.setRequestHandler(ListResourcesRequestSchema, async (req) => {
130
+ logger.debug("Received ListResources request");
131
+ return { resources: [] };
132
+ });
133
+ // Register CallTool handler with proper logging
134
+ logger.info("Registering tool handlers", {
135
+ toolCount: 36,
136
+ categories: ["workspace", "task", "time-tracking", "list", "folder", "tag", "member", "document"]
137
+ });
138
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
139
+ const { name, arguments: params } = req.params;
140
+ // Improved logging with more context
141
+ logger.info(`Received CallTool request for tool: ${name}`, {
142
+ params
143
+ });
144
+ // Check if the tool is enabled
145
+ if (!isToolEnabled(name)) {
146
+ const reason = config.enabledTools.length > 0
147
+ ? `Tool '${name}' is not in the enabled tools list.`
148
+ : `Tool '${name}' is disabled.`;
149
+ logger.warn(`Tool execution blocked: ${reason}`);
150
+ throw {
151
+ code: -32601,
152
+ message: reason
153
+ };
154
+ }
155
+ try {
156
+ // Handle tool calls by routing to the appropriate handler
157
+ switch (name) {
158
+ case "get_workspace_hierarchy":
159
+ return handleGetWorkspaceHierarchy();
160
+ case "create_task":
161
+ return handleCreateTask(params);
162
+ case "update_task":
163
+ return handleUpdateTask(params);
164
+ case "move_task":
165
+ return handleMoveTask(params);
166
+ case "duplicate_task":
167
+ return handleDuplicateTask(params);
168
+ case "get_task":
169
+ return handleGetTask(params);
170
+ case "delete_task":
171
+ return handleDeleteTask(params);
172
+ case "get_task_comments":
173
+ return handleGetTaskComments(params);
174
+ case "create_task_comment":
175
+ return handleCreateTaskComment(params);
176
+ case "attach_task_file":
177
+ return handleAttachTaskFile(params);
178
+ case "create_bulk_tasks":
179
+ return handleCreateBulkTasks(params);
180
+ case "update_bulk_tasks":
181
+ return handleUpdateBulkTasks(params);
182
+ case "move_bulk_tasks":
183
+ return handleMoveBulkTasks(params);
184
+ case "delete_bulk_tasks":
185
+ return handleDeleteBulkTasks(params);
186
+ case "get_workspace_tasks":
187
+ return handleGetWorkspaceTasks(params);
188
+ case "create_list":
189
+ return handleCreateList(params);
190
+ case "create_list_in_folder":
191
+ return handleCreateListInFolder(params);
192
+ case "get_list":
193
+ return handleGetList(params);
194
+ case "update_list":
195
+ return handleUpdateList(params);
196
+ case "delete_list":
197
+ return handleDeleteList(params);
198
+ case "create_folder":
199
+ return handleCreateFolder(params);
200
+ case "get_folder":
201
+ return handleGetFolder(params);
202
+ case "update_folder":
203
+ return handleUpdateFolder(params);
204
+ case "delete_folder":
205
+ return handleDeleteFolder(params);
206
+ case "get_space_tags":
207
+ return handleGetSpaceTags(params);
208
+ case "add_tag_to_task":
209
+ return handleAddTagToTask(params);
210
+ case "remove_tag_from_task":
211
+ return handleRemoveTagFromTask(params);
212
+ case "get_task_time_entries":
213
+ return handleGetTaskTimeEntries(params);
214
+ case "start_time_tracking":
215
+ return handleStartTimeTracking(params);
216
+ case "stop_time_tracking":
217
+ return handleStopTimeTracking(params);
218
+ case "add_time_entry":
219
+ return handleAddTimeEntry(params);
220
+ case "delete_time_entry":
221
+ return handleDeleteTimeEntry(params);
222
+ case "get_current_time_entry":
223
+ return handleGetCurrentTimeEntry(params);
224
+ case "create_document":
225
+ return handleCreateDocument(params);
226
+ case "get_document":
227
+ return handleGetDocument(params);
228
+ case "list_documents":
229
+ return handleListDocuments(params);
230
+ case "list_document_pages":
231
+ return handleListDocumentPages(params);
232
+ case "get_document_pages":
233
+ return handleGetDocumentPages(params);
234
+ case "create_document_page":
235
+ return handleCreateDocumentPage(params);
236
+ case "update_document_page":
237
+ return handleUpdateDocumentPage(params);
238
+ case "get_workspace_members":
239
+ return handleGetWorkspaceMembers();
240
+ case "find_member_by_name":
241
+ return handleFindMemberByName(params);
242
+ case "resolve_assignees":
243
+ return handleResolveAssignees(params);
244
+ default:
245
+ logger.error(`Unknown tool requested: ${name}`);
246
+ const error = new Error(`Unknown tool: ${name}`);
247
+ error.name = "UnknownToolError";
248
+ throw error;
249
+ }
250
+ }
251
+ catch (err) {
252
+ logger.error(`Error executing tool: ${name}`, err);
253
+ // Transform error to a more descriptive JSON-RPC error
254
+ if (err.name === "UnknownToolError") {
255
+ throw {
256
+ code: -32601,
257
+ message: `Method not found: ${name}`
258
+ };
259
+ }
260
+ else if (err.name === "ValidationError") {
261
+ throw {
262
+ code: -32602,
263
+ message: `Invalid params for tool ${name}: ${err.message}`
264
+ };
265
+ }
266
+ else {
267
+ // Generic server error
268
+ throw {
269
+ code: -32000,
270
+ message: `Error executing tool ${name}: ${err.message}`
271
+ };
272
+ }
273
+ }
274
+ });
275
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
276
+ logger.info("Received ListPrompts request");
277
+ return { prompts: [] };
278
+ });
279
+ server.setRequestHandler(GetPromptRequestSchema, async () => {
280
+ logger.error("Received GetPrompt request, but prompts are not supported");
281
+ throw new Error("Prompt not found");
282
+ });
283
+ return server;
284
+ }
285
+ /**
286
+ * Export the clickup service for use in tool handlers
287
+ */
288
+ export { workspace };