@taazkareem/clickup-mcp-server 0.7.1 → 0.8.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.
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.7.1 now available with complete Time Tracking support and Document Management features.
9
+ > 🚀 **Status Update:** v0.8.0 is now available with HTTP Streamable transport support, Legacy SSE Support, Member Management tools, and more.
10
10
 
11
11
  ## Setup
12
12
 
@@ -59,17 +59,86 @@ Additionally, you can use the `DISABLED_TOOLS` environment variable or `--env DI
59
59
 
60
60
  Please disable tools you don't need if you are having issues with the number of tools or any context limitations
61
61
 
62
+ ## Running with HTTP Transport Support
63
+
64
+ The server supports both modern **HTTP Streamable** transport (MCP Inspector compatible) and legacy **SSE (Server-Sent Events)** transport for backwards compatibility.
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "ClickUp": {
70
+ "command": "npx",
71
+ "args": [
72
+ "-y",
73
+ "@taazkareem/clickup-mcp-server@latest"
74
+ ],
75
+ "env": {
76
+ "CLICKUP_API_KEY": "your-api-key",
77
+ "CLICKUP_TEAM_ID": "your-team-id",
78
+ "ENABLE_SSE": "true",
79
+ "PORT": "3231"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ **Endpoints:**
87
+ - **Primary**: `http://127.0.0.1:3231/mcp` (Streamable HTTP)
88
+ - **Legacy**: `http://127.0.0.1:3231/sse` (SSE for backwards compatibility)
89
+
90
+ ### Command Line Usage
91
+
92
+ ```bash
93
+ npx -y @taazkareem/clickup-mcp-server@latest --env CLICKUP_API_KEY=your-api-key --env CLICKUP_TEAM_ID=your-team-id --env ENABLE_SSE=true --env PORT=3231
94
+ ```
95
+
96
+ Available configuration options:
97
+
98
+ | Option | Description | Default |
99
+ | ------ | ----------- | ------- |
100
+ | `ENABLE_SSE` | Enable the HTTP/SSE transport | `false` |
101
+ | `PORT` | Port for the HTTP server | `3231` |
102
+ | `ENABLE_STDIO` | Enable the STDIO transport | `true` |
103
+
104
+ #### n8n Integration
105
+
106
+ To integrate with n8n:
107
+
108
+ 1. Start the clickup-mcp-server with SSE enabled
109
+ 2. In n8n, add a new "MCP AI Tool" node
110
+ 3. Configure the node with:
111
+ - Transport: SSE
112
+ - Server URL: `http://localhost:3231` (or your server address)
113
+ - Tools: Select the ClickUp tools you want to use
114
+
115
+ #### Example Client
116
+
117
+ An example SSE client is provided in the `examples` directory. To run it:
118
+
119
+ ```bash
120
+ # Start the server with SSE enabled
121
+ ENABLE_SSE=true PORT=3231 npx -y @taazkareem/clickup-mcp-server@latest --env CLICKUP_API_KEY=your-api-key --env CLICKUP_TEAM_ID=your-team-id
122
+
123
+ # In another terminal, run the example client
124
+ cd examples
125
+ npm install
126
+ npm run sse-client
127
+ ```
128
+
62
129
  ## Features
63
130
 
64
- | 📝 Task Management | 🏷️ Tag Management |
65
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
- | • Create, update, and delete tasks `<br>`• Move and duplicate tasks anywhere `<br>`• Support for single and bulk operations `<br>`• Set start/due dates with natural language `<br>`• Create and manage subtasks `<br>`• Add comments and attachments | • Create, update, and delete space tags `<br>`• Add and remove tags from tasks `<br>`• Use natural language color commands `<br>`• Automatic contrasting foreground colors `<br>`• View all space tags `<br>`• Tag-based task organization across workspace |
67
- | ⏱️**Time Tracking** | 🌳**Workspace Organization** |
68
- | • View time entries for tasks `<br>`• Start/stop time tracking on tasks `<br>`• Add manual time entries `<br>`• Delete time entries `<br>`• View currently running timer `<br>`• Track billable and non-billable time | • Navigate spaces, folders, and lists `<br>`• Create and manage folders `<br>`• Organize lists within spaces `<br>`• Create lists in folders `<br>`• View workspace hierarchy `<br>`• Efficient path navigation |
69
- | ⚡**Integration Features** | **Document Listing, Creation and Updating!** |
70
- | • Global name or ID-based lookups `<br>`• Case-insensitive matching `<br>`• Markdown formatting support `<br>`• Built-in rate limiting `<br>`• Error handling and validation `<br>`• Comprehensive API coverage | Document Listing through all workspace `<br>` Document Page listing `<br>` Document Page Details `<br>` Document Creation `<br>` Document page update, modification (append and prepend) `<br>` |
131
+ | 📝 Task Management | 🏷️ Tag Management |
132
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133
+ | • Create, update, and delete tasks<br>• Move and duplicate tasks anywhere<br>• Support for single and bulk operations<br>• Set start/due dates with natural language<br>• Create and manage subtasks<br>• Add comments and attachments | • Create, update, and delete space tags<br>• Add and remove tags from tasks<br>• Use natural language color commands<br>• Automatic contrasting foreground colors<br>• View all space tags<br>• Tag-based task organization across workspace |
134
+ | ⏱️ **Time Tracking** | 🌳 **Workspace Organization** |
135
+ | • View time entries for tasks<br>• Start/stop time tracking on tasks<br>• Add manual time entries<br>• Delete time entries<br>• View currently running timer<br>• Track billable and non-billable time | • Navigate spaces, folders, and lists<br>• Create and manage folders<br>• Organize lists within spaces<br>• Create lists in folders<br>• View workspace hierarchy<br>• Efficient path navigation |
136
+ | 📄 **Document Management** | 👥 **Member Management** |
137
+ | • Document Listing through all workspace<br>• Document Page listing<br>• Document Page Details<br>• Document Creation<br>• Document page update (append & prepend) | Find workspace members by name or email<br>• Resolve assignees for tasks<br>• View member details and permissions<br>• Assign tasks to users during creation and updates<br>• Support for user IDs, emails, or usernames<br>• Team-wide user management |
138
+ | ⚡ **Integration Features** | 🏗️ **Architecture & Performance** |
139
+ | • Global name or ID-based lookups<br>• Case-insensitive matching<br>• Markdown formatting support<br>• Built-in rate limiting<br>• Error handling and validation<br>• Comprehensive API coverage | • **70% codebase reduction** for improved performance<br>• **Unified architecture** across all transport types<br>• **Zero code duplication**<br>• **HTTP Streamable transport** (MCP Inspector compatible)<br>• **Legacy SSE support** for backwards compatibility |
71
140
 
72
- ## Available Tools
141
+ ## Available Tools (36 Total)
73
142
 
74
143
  | Tool | Description | Required Parameters |
75
144
  | ------------------------------------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -110,6 +179,9 @@ Please disable tools you don't need if you are having issues with the number of
110
179
  | [add_time_entry](docs/api-reference.md#time-tracking) | Add manual time entry to a task | `taskId`/`taskName`, `start`, `duration` |
111
180
  | [delete_time_entry](docs/api-reference.md#time-tracking) | Delete a time entry | `timeEntryId` |
112
181
  | [get_current_time_entry](docs/api-reference.md#time-tracking) | Get currently running timer | None |
182
+ | [get_workspace_members](docs/api-reference.md#member-management) | Get all workspace members | None |
183
+ | [find_member_by_name](docs/api-reference.md#member-management) | Find member by name or email | `nameOrEmail` |
184
+ | [resolve_assignees](docs/api-reference.md#member-management) | Resolve member names to IDs | `assignees[]` |
113
185
  | [create_document](docs/api-reference.md#document-management) | Create a document | `workspaceId`, `name`, `parentId`/`parentType`, `visibility`, `create_pages` |
114
186
  | [get_document](docs/api-reference.md#document-management) | Get a document | `workspaceId`/`documentId` |
115
187
  | [list_documents](docs/api-reference.md#document-management) | List documents | `workspaceId`, `documentId`/`creator`/`deleted`/`archived`/`parent_id`/`parent_type`/`limit`/`next_cursor` |
@@ -120,6 +192,29 @@ Please disable tools you don't need if you are having issues with the number of
120
192
 
121
193
  See [full documentation](docs/api-reference.md) for optional parameters and advanced usage.
122
194
 
195
+ ## Member Management Tools
196
+
197
+ When creating or updating tasks, you can assign users using the `assignees` parameter. The parameter accepts an array of user IDs, emails, or usernames:
198
+
199
+ **Creating tasks with assignees:**
200
+ ```json
201
+ {
202
+ "name": "New Task",
203
+ "description": "This is a new task.",
204
+ "assignees": ["jdoe@example.com", "Jane Smith"] // Emails, usernames, or user IDs
205
+ }
206
+ ```
207
+
208
+ **Updating task assignees:**
209
+ ```json
210
+ {
211
+ "taskId": "abc123",
212
+ "assignees": ["newuser@example.com"] // Replace existing assignees
213
+ }
214
+ ```
215
+
216
+ The member management tools help resolve user references when needed.
217
+
123
218
  ## Prompts
124
219
 
125
220
  Not yet implemented and not supported by all client apps. Request a feature for a Prompt implementation that would be most beneficial for your workflow (without it being too specific). Examples:
@@ -175,4 +270,4 @@ This software makes use of third-party APIs and may reference trademarks
175
270
  or brands owned by third parties. The use of such APIs or references does not imply
176
271
  any affiliation with or endorsement by the respective companies. All trademarks and
177
272
  brand names are the property of their respective owners. This project is an independent
178
- work and is not officially associated with or sponsored by any third-party company mentioned.
273
+ work and is not officially associated with or sponsored by any third-party company mentioned.
package/build/config.js CHANGED
@@ -11,6 +11,11 @@
11
11
  * The document support is optional and can be passed via command line arguments.
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
+ *
15
+ * Server transport options:
16
+ * - ENABLE_SSE: Enable Server-Sent Events transport (default: false)
17
+ * - SSE_PORT: Port for SSE server (default: 3000)
18
+ * - ENABLE_STDIO: Enable STDIO transport (default: true)
14
19
  */
15
20
  // Parse any command line environment arguments
16
21
  const args = process.argv.slice(2);
@@ -34,6 +39,14 @@ for (let i = 0; i < args.length; i++) {
34
39
  envArgs.disabledTools = value;
35
40
  if (key === 'DISABLED_COMMANDS')
36
41
  envArgs.disabledTools = value; // Backward compatibility
42
+ if (key === 'ENABLE_SSE')
43
+ envArgs.enableSSE = value;
44
+ if (key === 'SSE_PORT')
45
+ envArgs.ssePort = value;
46
+ if (key === 'ENABLE_STDIO')
47
+ envArgs.enableStdio = value;
48
+ if (key === 'PORT')
49
+ envArgs.port = value;
37
50
  i++;
38
51
  }
39
52
  }
@@ -61,6 +74,19 @@ export const parseLogLevel = (levelStr) => {
61
74
  return LogLevel.ERROR;
62
75
  }
63
76
  };
77
+ // Parse boolean string
78
+ const parseBoolean = (value, defaultValue) => {
79
+ if (value === undefined)
80
+ return defaultValue;
81
+ return value.toLowerCase() === 'true';
82
+ };
83
+ // Parse integer string
84
+ const parseInteger = (value, defaultValue) => {
85
+ if (value === undefined)
86
+ return defaultValue;
87
+ const parsed = parseInt(value, 10);
88
+ return isNaN(parsed) ? defaultValue : parsed;
89
+ };
64
90
  // Load configuration from command line args or environment variables
65
91
  const configuration = {
66
92
  clickupApiKey: envArgs.clickupApiKey || process.env.CLICKUP_API_KEY || '',
@@ -69,6 +95,10 @@ const configuration = {
69
95
  documentSupport: envArgs.documentSupport || process.env.DOCUMENT_SUPPORT || process.env.DOCUMENT_MODULE || process.env.DOCUMENT_MODEL || 'false',
70
96
  logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL),
71
97
  disabledTools: ((envArgs.disabledTools || process.env.DISABLED_TOOLS || process.env.DISABLED_COMMANDS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
98
+ enableSSE: parseBoolean(envArgs.enableSSE || process.env.ENABLE_SSE, false),
99
+ ssePort: parseInteger(envArgs.ssePort || process.env.SSE_PORT, 3000),
100
+ enableStdio: parseBoolean(envArgs.enableStdio || process.env.ENABLE_STDIO, true),
101
+ port: envArgs.port || process.env.PORT || '3231',
72
102
  };
73
103
  // Don't log to console as it interferes with JSON-RPC communication
74
104
  // Validate only the required variables are present
package/build/index.js CHANGED
@@ -18,14 +18,17 @@
18
18
  * - Name-based entity resolution
19
19
  * - Markdown formatting
20
20
  * - Built-in rate limiting
21
+ * - Multiple transport options (STDIO, SSE, HTTP Streamable)
21
22
  *
22
23
  * For full documentation and usage examples, please refer to the README.md file.
23
24
  */
24
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
- import { configureServer, server } from "./server.js";
26
- import { info, error } from "./logger.js";
25
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
26
+ import { configureServer, server } from './server.js';
27
+ import { info, error } from './logger.js';
28
+ import config from './config.js';
27
29
  import { dirname } from 'path';
28
30
  import { fileURLToPath } from 'url';
31
+ import { startSSEServer } from './sse_server.js';
29
32
  // Get directory name for module paths
30
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
31
34
  // Handle uncaught exceptions
@@ -38,30 +41,43 @@ process.on('unhandledRejection', (reason, promise) => {
38
41
  error("Unhandled Rejection", { reason });
39
42
  process.exit(1);
40
43
  });
44
+ async function startStdioServer() {
45
+ info('Starting ClickUp MCP Server...');
46
+ // Log essential information about the environment
47
+ info('Server environment', {
48
+ pid: process.pid,
49
+ node: process.version,
50
+ os: process.platform,
51
+ arch: process.arch,
52
+ });
53
+ // Configure the server with all handlers
54
+ info('Configuring server request handlers');
55
+ await configureServer();
56
+ // Connect using stdio transport
57
+ info('Connecting to MCP stdio transport');
58
+ const transport = new StdioServerTransport();
59
+ await server.connect(transport);
60
+ info('Server startup complete - ready to handle requests');
61
+ }
41
62
  /**
42
63
  * Application entry point that configures and starts the MCP server.
43
64
  */
44
65
  async function main() {
45
66
  try {
46
- info("Starting ClickUp MCP Server...");
47
- // Log essential information about the environment
48
- info("Server environment", {
49
- pid: process.pid,
50
- node: process.version,
51
- os: process.platform,
52
- arch: process.arch
53
- });
54
- // Configure the server with all handlers
55
- info("Configuring server request handlers");
56
- await configureServer();
57
- // Connect using stdio transport
58
- info("Connecting to MCP stdio transport");
59
- const transport = new StdioServerTransport();
60
- await server.connect(transport);
61
- info("Server startup complete - ready to handle requests");
67
+ if (config.enableSSE) {
68
+ // Start the new SSE server with HTTP Streamable support
69
+ startSSEServer();
70
+ }
71
+ else {
72
+ // Start the traditional STDIO server
73
+ await startStdioServer();
74
+ }
62
75
  }
63
76
  catch (err) {
64
- error("Error during server startup", { message: err.message, stack: err.stack });
77
+ error('Error during server startup', {
78
+ message: err.message,
79
+ stack: err.stack,
80
+ });
65
81
  process.exit(1);
66
82
  }
67
83
  }
@@ -0,0 +1,13 @@
1
+ // src/schemas/member.ts
2
+ import { z } from 'zod';
3
+ export const MemberSchema = z.object({
4
+ id: z.number(),
5
+ username: z.string().optional(),
6
+ email: z.string().optional(),
7
+ full_name: z.string().optional(),
8
+ profile_picture: z.string().optional(),
9
+ role: z.number(),
10
+ role_name: z.string().optional(),
11
+ initials: z.string().optional(),
12
+ last_active: z.string().optional(),
13
+ });
package/build/server.js CHANGED
@@ -13,6 +13,7 @@ import { createListTool, handleCreateList, createListInFolderTool, handleCreateL
13
13
  import { createFolderTool, handleCreateFolder, getFolderTool, handleGetFolder, updateFolderTool, handleUpdateFolder, deleteFolderTool, handleDeleteFolder } from "./tools/folder.js";
14
14
  import { getSpaceTagsTool, handleGetSpaceTags, addTagToTaskTool, handleAddTagToTask, removeTagFromTaskTool, handleRemoveTagFromTask } from "./tools/tag.js";
15
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";
16
17
  import { Logger } from "./logger.js";
17
18
  import { clickUpServices } from "./services/shared.js";
18
19
  // Create a logger instance for server
@@ -21,7 +22,7 @@ const logger = new Logger('Server');
21
22
  const { workspace } = clickUpServices;
22
23
  export const server = new Server({
23
24
  name: "clickup-mcp-server",
24
- version: "0.7.1",
25
+ version: "0.8.0",
25
26
  }, {
26
27
  capabilities: {
27
28
  tools: {},
@@ -88,6 +89,9 @@ export function configureServer() {
88
89
  getSpaceTagsTool,
89
90
  addTagToTaskTool,
90
91
  removeTagFromTaskTool,
92
+ getWorkspaceMembersTool,
93
+ findMemberByNameTool,
94
+ resolveAssigneesTool,
91
95
  ...documentModule()
92
96
  ].filter(tool => !config.disabledTools.includes(tool.name))
93
97
  };
@@ -99,8 +103,8 @@ export function configureServer() {
99
103
  });
100
104
  // Register CallTool handler with proper logging
101
105
  logger.info("Registering tool handlers", {
102
- toolCount: 40,
103
- categories: ["workspace", "task", "time-tracking", "list", "folder", "tag", "document"]
106
+ toolCount: 36,
107
+ categories: ["workspace", "task", "time-tracking", "list", "folder", "tag", "member", "document"]
104
108
  });
105
109
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
106
110
  const { name, arguments: params } = req.params;
@@ -199,6 +203,12 @@ export function configureServer() {
199
203
  return handleCreateDocumentPage(params);
200
204
  case "update_document_page":
201
205
  return handleUpdateDocumentPage(params);
206
+ case "get_workspace_members":
207
+ return handleGetWorkspaceMembers();
208
+ case "find_member_by_name":
209
+ return handleFindMemberByName(params);
210
+ case "resolve_assignees":
211
+ return handleResolveAssignees(params);
202
212
  default:
203
213
  logger.error(`Unknown tool requested: ${name}`);
204
214
  const error = new Error(`Unknown tool: ${name}`);
@@ -366,4 +366,32 @@ export class WorkspaceService extends BaseClickUpService {
366
366
  throw this.handleError(error, `Failed to get lists in folder ${folderId}`);
367
367
  }
368
368
  }
369
+ /**
370
+ * Get all members in a workspace
371
+ * @returns Array of workspace members
372
+ */
373
+ async getWorkspaceMembers() {
374
+ try {
375
+ // Use the existing team/workspace endpoint which typically returns member information
376
+ const teamId = this.teamId;
377
+ const response = await this.client.get(`/team/${teamId}`);
378
+ if (!response || !response.data || !response.data.team) {
379
+ throw new Error('Invalid response from ClickUp API');
380
+ }
381
+ // Extract and normalize member data
382
+ const members = response.data.team.members || [];
383
+ return members.map((member) => ({
384
+ id: member.user?.id,
385
+ name: member.user?.username || member.user?.email,
386
+ username: member.user?.username,
387
+ email: member.user?.email,
388
+ role: member.role,
389
+ profilePicture: member.user?.profilePicture
390
+ }));
391
+ }
392
+ catch (error) {
393
+ console.error('Error getting workspace members:', error);
394
+ throw error;
395
+ }
396
+ }
369
397
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * SSE and HTTP Streamable Transport Server
6
+ *
7
+ * This module provides HTTP Streamable and legacy SSE transport support
8
+ * for the ClickUp MCP Server. It reuses the unified server configuration
9
+ * from server.ts to avoid code duplication.
10
+ */
11
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
12
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
14
+ import express from 'express';
15
+ import { server, configureServer } from './server.js';
16
+ import configuration from './config.js';
17
+ const app = express();
18
+ app.use(express.json());
19
+ export function startSSEServer() {
20
+ // Configure the unified server first
21
+ configureServer();
22
+ const transports = {
23
+ streamable: {},
24
+ sse: {},
25
+ };
26
+ // Streamable HTTP endpoint - handles POST requests for client-to-server communication
27
+ app.post('/mcp', async (req, res) => {
28
+ try {
29
+ const sessionId = req.headers['mcp-session-id'];
30
+ let transport;
31
+ if (sessionId && transports.streamable[sessionId]) {
32
+ transport = transports.streamable[sessionId];
33
+ }
34
+ else if (!sessionId && isInitializeRequest(req.body)) {
35
+ transport = new StreamableHTTPServerTransport({
36
+ sessionIdGenerator: () => `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`,
37
+ onsessioninitialized: (sessionId) => {
38
+ transports.streamable[sessionId] = transport;
39
+ }
40
+ });
41
+ transport.onclose = () => {
42
+ if (transport.sessionId) {
43
+ delete transports.streamable[transport.sessionId];
44
+ }
45
+ };
46
+ await server.connect(transport);
47
+ }
48
+ else {
49
+ res.status(400).json({
50
+ jsonrpc: '2.0',
51
+ error: {
52
+ code: -32000,
53
+ message: 'Bad Request: No valid session ID provided',
54
+ },
55
+ id: null,
56
+ });
57
+ return;
58
+ }
59
+ await transport.handleRequest(req, res, req.body);
60
+ }
61
+ catch (error) {
62
+ console.error('Error handling MCP request:', error);
63
+ if (!res.headersSent) {
64
+ res.status(500).json({
65
+ jsonrpc: '2.0',
66
+ error: {
67
+ code: -32603,
68
+ message: 'Internal server error',
69
+ },
70
+ id: null,
71
+ });
72
+ }
73
+ }
74
+ });
75
+ const handleSessionRequest = async (req, res) => {
76
+ const sessionId = req.headers['mcp-session-id'];
77
+ if (!sessionId || !transports.streamable[sessionId]) {
78
+ res.status(400).send('Invalid or missing session ID');
79
+ return;
80
+ }
81
+ const transport = transports.streamable[sessionId];
82
+ await transport.handleRequest(req, res);
83
+ };
84
+ app.get('/mcp', handleSessionRequest);
85
+ app.delete('/mcp', handleSessionRequest);
86
+ // Legacy SSE endpoints (for backwards compatibility)
87
+ app.get('/sse', async (req, res) => {
88
+ const transport = new SSEServerTransport('/messages', res);
89
+ transports.sse[transport.sessionId] = transport;
90
+ console.log(`New SSE connection established with sessionId: ${transport.sessionId}`);
91
+ res.on('close', () => {
92
+ delete transports.sse[transport.sessionId];
93
+ });
94
+ await server.connect(transport);
95
+ });
96
+ app.post('/messages', async (req, res) => {
97
+ const sessionId = req.query.sessionId;
98
+ const transport = transports.sse[sessionId];
99
+ if (transport) {
100
+ await transport.handlePostMessage(req, res, req.body);
101
+ }
102
+ else {
103
+ res.status(400).send('No transport found for sessionId');
104
+ }
105
+ });
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`);
112
+ });
113
+ }
@@ -13,3 +13,4 @@ export * from './task/index.js';
13
13
  export * from './list.js';
14
14
  export * from './folder.js';
15
15
  export * from './tag.js';
16
+ export * from './member.js';
@@ -0,0 +1,108 @@
1
+ import { workspaceService } from '../services/shared.js';
2
+ import { sponsorService } from '../utils/sponsor-service.js';
3
+ /**
4
+ * Tool definition for getting all members in a ClickUp workspace
5
+ */
6
+ export const getWorkspaceMembersTool = {
7
+ name: 'get_workspace_members',
8
+ description: 'Returns all members (users) in the ClickUp workspace/team. Useful for resolving assignees by name or email.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {},
12
+ required: []
13
+ }
14
+ };
15
+ /**
16
+ * Tool definition for finding a member by name or email
17
+ */
18
+ export const findMemberByNameTool = {
19
+ name: 'find_member_by_name',
20
+ description: 'Finds a member in the ClickUp workspace by name or email. Returns the member object if found, or null if not found.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ nameOrEmail: {
25
+ type: 'string',
26
+ description: 'The name or email of the member to find.'
27
+ }
28
+ },
29
+ required: ['nameOrEmail']
30
+ }
31
+ };
32
+ /**
33
+ * Tool definition for resolving an array of assignee names/emails to ClickUp user IDs
34
+ */
35
+ export const resolveAssigneesTool = {
36
+ name: 'resolve_assignees',
37
+ description: 'Resolves an array of assignee names or emails to ClickUp user IDs. Returns an array of user IDs, or errors for any that cannot be resolved.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ assignees: {
42
+ type: 'array',
43
+ items: { type: 'string' },
44
+ description: 'Array of assignee names or emails to resolve.'
45
+ }
46
+ },
47
+ required: ['assignees']
48
+ }
49
+ };
50
+ /// src/tools/member.ts
51
+ /**
52
+ * Handler for get_workspace_members
53
+ */
54
+ export async function handleGetWorkspaceMembers() {
55
+ try {
56
+ const members = await workspaceService.getWorkspaceMembers();
57
+ return sponsorService.createResponse({ members }, true);
58
+ }
59
+ catch (error) {
60
+ const errorMessage = error instanceof Error ? error.message : String(error);
61
+ return sponsorService.createErrorResponse(`Failed to get workspace members: ${errorMessage}`);
62
+ }
63
+ }
64
+ /**
65
+ * Handler for find_member_by_name
66
+ */
67
+ export async function handleFindMemberByName(parameters) {
68
+ const { nameOrEmail } = parameters;
69
+ if (!nameOrEmail) {
70
+ throw new Error('nameOrEmail is required');
71
+ }
72
+ try {
73
+ const members = await workspaceService.getWorkspaceMembers();
74
+ const found = members.find((m) => m.email?.toLowerCase() === nameOrEmail.toLowerCase() ||
75
+ m.username?.toLowerCase() === nameOrEmail.toLowerCase() ||
76
+ m.name?.toLowerCase() === nameOrEmail.toLowerCase());
77
+ return sponsorService.createResponse({ member: found || null }, true);
78
+ }
79
+ catch (error) {
80
+ const errorMessage = error instanceof Error ? error.message : String(error);
81
+ return sponsorService.createErrorResponse(`Failed to find member: ${errorMessage}`);
82
+ }
83
+ }
84
+ /**
85
+ * Handler for resolve_assignees
86
+ */
87
+ export async function handleResolveAssignees(parameters) {
88
+ const { assignees } = parameters;
89
+ if (!Array.isArray(assignees)) {
90
+ throw new Error('assignees must be an array');
91
+ }
92
+ try {
93
+ const members = await workspaceService.getWorkspaceMembers();
94
+ const resolved = assignees.map((input) => {
95
+ const found = members.find((m) => m.email?.toLowerCase() === input.toLowerCase() ||
96
+ m.username?.toLowerCase() === input.toLowerCase() ||
97
+ m.name?.toLowerCase() === input.toLowerCase());
98
+ return found ? found.id : null;
99
+ });
100
+ // Return a plain object, not wrapped in sponsorService.createResponse
101
+ return { userIds: resolved };
102
+ }
103
+ catch (error) {
104
+ const errorMessage = error instanceof Error ? error.message : String(error);
105
+ // Return a plain error object
106
+ return { error: `Failed to resolve assignees: ${errorMessage}` };
107
+ }
108
+ }
@@ -73,7 +73,7 @@ const taskIdentifierSchema = {
73
73
  */
74
74
  export const createBulkTasksTool = {
75
75
  name: "create_bulk_tasks",
76
- description: `Creates multiple tasks in one list. Use listId (preferred) or listName + array of tasks (each needs name). Configure batch size/concurrency via options. Tasks can have custom fields as {id, value} array.`,
76
+ description: `Creates multiple tasks in one list. Use listId (preferred) or listName + array of tasks (each needs name). Configure batch size/concurrency via options. Tasks can have custom fields as {id, value} array and assignees as array of user IDs, emails, or usernames.`,
77
77
  inputSchema: {
78
78
  type: "object",
79
79
  properties: {
@@ -130,6 +130,16 @@ export const createBulkTasksTool = {
130
130
  required: ["id", "value"]
131
131
  },
132
132
  description: "Optional array of custom field values to set on the task."
133
+ },
134
+ assignees: {
135
+ type: "array",
136
+ items: {
137
+ oneOf: [
138
+ { type: "number" },
139
+ { type: "string" }
140
+ ]
141
+ },
142
+ description: "Optional array of assignee user IDs (numbers), emails, or usernames to assign to the task."
133
143
  }
134
144
  },
135
145
  required: ["name"]
@@ -153,7 +163,7 @@ export const createBulkTasksTool = {
153
163
  */
154
164
  export const updateBulkTasksTool = {
155
165
  name: "update_bulk_tasks",
156
- description: `Updates multiple tasks efficiently. For each task: use taskId (preferred) or taskName + listName. At least one update field per task. Configure batch size/concurrency via options. WARNING: taskName without listName will fail.`,
166
+ description: `Updates multiple tasks efficiently. For each task: use taskId (preferred) or taskName + listName. At least one update field per task. Supports assignees as array of user IDs, emails, or usernames. Configure batch size/concurrency via options. WARNING: taskName without listName will fail.`,
157
167
  inputSchema: {
158
168
  type: "object",
159
169
  properties: {
@@ -221,6 +231,16 @@ export const updateBulkTasksTool = {
221
231
  required: ["id", "value"]
222
232
  },
223
233
  description: "Optional array of custom field values to set on the task."
234
+ },
235
+ assignees: {
236
+ type: "array",
237
+ items: {
238
+ oneOf: [
239
+ { type: "number" },
240
+ { type: "string" }
241
+ ]
242
+ },
243
+ description: "Optional array of assignee user IDs (numbers), emails, or usernames to assign to the task."
224
244
  }
225
245
  }
226
246
  }
@@ -51,6 +51,34 @@ function getCachedTaskContext(taskName) {
51
51
  //=============================================================================
52
52
  // SHARED UTILITY FUNCTIONS
53
53
  //=============================================================================
54
+ /**
55
+ * Parse time estimate string into minutes
56
+ * Supports formats like "2h 30m", "150m", "2.5h"
57
+ */
58
+ function parseTimeEstimate(timeEstimate) {
59
+ // If it's already a number, return it directly
60
+ if (typeof timeEstimate === 'number') {
61
+ return timeEstimate;
62
+ }
63
+ if (!timeEstimate || typeof timeEstimate !== 'string')
64
+ return 0;
65
+ // If it's just a number as string, parse it
66
+ if (/^\d+$/.test(timeEstimate)) {
67
+ return parseInt(timeEstimate, 10);
68
+ }
69
+ let totalMinutes = 0;
70
+ // Extract hours
71
+ const hoursMatch = timeEstimate.match(/(\d+\.?\d*)h/);
72
+ if (hoursMatch) {
73
+ totalMinutes += parseFloat(hoursMatch[1]) * 60;
74
+ }
75
+ // Extract minutes
76
+ const minutesMatch = timeEstimate.match(/(\d+)m/);
77
+ if (minutesMatch) {
78
+ totalMinutes += parseInt(minutesMatch[1], 10);
79
+ }
80
+ return Math.round(totalMinutes); // Return minutes
81
+ }
54
82
  /**
55
83
  * Build task update data from parameters
56
84
  */
@@ -75,6 +103,15 @@ function buildUpdateData(params) {
75
103
  updateData.start_date = parseDueDate(params.startDate);
76
104
  updateData.start_date_time = true;
77
105
  }
106
+ // Handle time estimate if provided - convert from string to minutes
107
+ if (params.time_estimate !== undefined) {
108
+ // Log the time estimate for debugging
109
+ console.log(`Original time_estimate: ${params.time_estimate}, typeof: ${typeof params.time_estimate}`);
110
+ // Parse and convert to number in minutes
111
+ const minutes = parseTimeEstimate(params.time_estimate);
112
+ console.log(`Converted time_estimate: ${minutes}`);
113
+ updateData.time_estimate = minutes;
114
+ }
78
115
  // Handle custom fields if provided
79
116
  if (params.custom_fields !== undefined) {
80
117
  updateData.custom_fields = params.custom_fields;
@@ -334,7 +371,7 @@ async function mapTaskIds(tasks) {
334
371
  * Handler for creating a task
335
372
  */
336
373
  export async function createTaskHandler(params) {
337
- const { name, description, markdown_description, status, dueDate, startDate, parent, tags, custom_fields, check_required_custom_fields } = params;
374
+ const { name, description, markdown_description, status, dueDate, startDate, parent, tags, custom_fields, check_required_custom_fields, assignees } = params;
338
375
  if (!name)
339
376
  throw new Error("Task name is required");
340
377
  // Use our helper function to validate and convert priority
@@ -349,7 +386,8 @@ export async function createTaskHandler(params) {
349
386
  parent,
350
387
  tags,
351
388
  custom_fields,
352
- check_required_custom_fields
389
+ check_required_custom_fields,
390
+ assignees
353
391
  };
354
392
  // Add due date if specified
355
393
  if (dueDate) {
@@ -568,7 +606,8 @@ export async function createBulkTasksHandler(params) {
568
606
  status: task.status,
569
607
  priority: toTaskPriority(task.priority),
570
608
  tags: task.tags,
571
- custom_fields: task.custom_fields
609
+ custom_fields: task.custom_fields,
610
+ assignees: task.assignees
572
611
  };
573
612
  // Add due date if specified
574
613
  if (task.dueDate) {
@@ -12,6 +12,8 @@ import { sponsorService } from '../../utils/sponsor-service.js';
12
12
  import { createTaskTool, getTaskTool, getTasksTool, updateTaskTool, moveTaskTool, duplicateTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool } from './single-operations.js';
13
13
  import { createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool } from './bulk-operations.js';
14
14
  import { getWorkspaceTasksTool } from './workspace-operations.js';
15
+ // Add this to your import statements at the top of the file
16
+ import { getWorkspaceMembersTool, findMemberByNameTool, resolveAssigneesTool, handleGetWorkspaceMembers, handleFindMemberByName, handleResolveAssignees } from '../member.js'; // Adjust the path as needed - it should point to where member.ts is located
15
17
  // Import handlers
16
18
  import { createTaskHandler, getTaskHandler, getTasksHandler, updateTaskHandler, moveTaskHandler, duplicateTaskHandler, deleteTaskHandler, getTaskCommentsHandler, createTaskCommentHandler, createBulkTasksHandler, updateBulkTasksHandler, moveBulkTasksHandler, deleteBulkTasksHandler, getWorkspaceTasksHandler, formatTaskData } from './index.js';
17
19
  // Import shared services
@@ -215,5 +217,17 @@ export const tools = [
215
217
  errors: result.failed.map(f => f.error)
216
218
  };
217
219
  }
220
+ },
221
+ {
222
+ definition: getWorkspaceMembersTool,
223
+ handler: handleGetWorkspaceMembers
224
+ },
225
+ {
226
+ definition: findMemberByNameTool,
227
+ handler: handleFindMemberByName
228
+ },
229
+ {
230
+ definition: resolveAssigneesTool,
231
+ handler: handleResolveAssignees
218
232
  }
219
233
  ];
@@ -48,7 +48,7 @@ const handleOperationError = (operation, error) => {
48
48
  */
49
49
  export const createTaskTool = {
50
50
  name: "create_task",
51
- description: `Creates a single task in a ClickUp list. Use listId (preferred) or listName. Required: name + list info. For multiple tasks use create_bulk_tasks. Can create subtasks via parent param. Supports custom fields as array of {id, value}.`,
51
+ description: `Creates a single task in a ClickUp list. Use listId (preferred) or listName. Required: name + list info. For multiple tasks use create_bulk_tasks. Can create subtasks via parent param. Supports custom fields as array of {id, value}. Supports assignees as array of user IDs, emails, or usernames.`,
52
52
  inputSchema: {
53
53
  type: "object",
54
54
  properties: {
@@ -119,6 +119,16 @@ export const createTaskTool = {
119
119
  check_required_custom_fields: {
120
120
  type: "boolean",
121
121
  description: "Optional flag to check if all required custom fields are set before saving the task."
122
+ },
123
+ assignees: {
124
+ type: "array",
125
+ items: {
126
+ oneOf: [
127
+ { type: "number" },
128
+ { type: "string" }
129
+ ]
130
+ },
131
+ description: "Optional array of assignee user IDs (numbers), emails, or usernames to assign to the task."
122
132
  }
123
133
  }
124
134
  }
@@ -128,7 +138,7 @@ export const createTaskTool = {
128
138
  */
129
139
  export const updateTaskTool = {
130
140
  name: "update_task",
131
- description: `Updates task properties. Use taskId (preferred) or taskName + optional listName. At least one update field required. Custom fields supported as array of {id, value}. WARNING: Using taskName without listName may match multiple tasks.`,
141
+ description: `Updates task properties. Use taskId (preferred) or taskName + optional listName. At least one update field required. Custom fields supported as array of {id, value}. Supports assignees as array of user IDs, emails, or usernames. WARNING: Using taskName without listName may match multiple tasks.`,
132
142
  inputSchema: {
133
143
  type: "object",
134
144
  properties: {
@@ -174,6 +184,10 @@ export const updateTaskTool = {
174
184
  type: "string",
175
185
  description: "New start date. Supports both Unix timestamps (in milliseconds) and natural language expressions."
176
186
  },
187
+ time_estimate: {
188
+ type: "string",
189
+ description: "Time estimate for the task. For best compatibility with the ClickUp API, use a numeric value in minutes (e.g., '150' for 2h 30m)"
190
+ },
177
191
  custom_fields: {
178
192
  type: "array",
179
193
  items: {
@@ -190,6 +204,16 @@ export const updateTaskTool = {
190
204
  required: ["id", "value"]
191
205
  },
192
206
  description: "Optional array of custom field values to set on the task. Each object must have an 'id' and 'value' property."
207
+ },
208
+ assignees: {
209
+ type: "array",
210
+ items: {
211
+ oneOf: [
212
+ { type: "number" },
213
+ { type: "string" }
214
+ ]
215
+ },
216
+ description: "Optional array of assignee user IDs (numbers), emails, or usernames to assign to the task."
193
217
  }
194
218
  }
195
219
  }
@@ -112,14 +112,6 @@ export function validateListIdentification(listId, listName) {
112
112
  * Ensures at least one update field is provided
113
113
  */
114
114
  export function validateTaskUpdateData(updateData) {
115
- // Check if there are any valid update fields present
116
- const hasUpdates = Object.keys(updateData).some(key => {
117
- return ['name', 'description', 'markdown_description', 'status', 'priority',
118
- 'dueDate', 'startDate', 'custom_fields'].includes(key);
119
- });
120
- if (!hasUpdates) {
121
- throw new Error("At least one field to update must be provided");
122
- }
123
115
  // Validate custom_fields if provided
124
116
  if (updateData.custom_fields) {
125
117
  if (!Array.isArray(updateData.custom_fields)) {
@@ -131,6 +123,10 @@ export function validateTaskUpdateData(updateData) {
131
123
  }
132
124
  }
133
125
  }
126
+ // Ensure there's at least one field to update
127
+ if (Object.keys(updateData).length === 0) {
128
+ throw new Error("At least one field to update must be provided");
129
+ }
134
130
  }
135
131
  /**
136
132
  * Validate bulk task array and task identification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -55,11 +55,16 @@
55
55
  },
56
56
  "homepage": "https://github.com/taazkareem/clickup-mcp-server#readme",
57
57
  "dependencies": {
58
- "@modelcontextprotocol/sdk": "^0.6.0",
58
+ "@modelcontextprotocol/sdk": "^1.11.3",
59
59
  "axios": "^1.6.7",
60
- "dotenv": "^16.4.1"
60
+ "cors": "^2.8.5",
61
+ "dotenv": "^16.5.0",
62
+ "express": "^5.1.0",
63
+ "zod": "^3.23.8"
61
64
  },
62
65
  "devDependencies": {
66
+ "@types/cors": "^2.8.17",
67
+ "@types/express": "^5.0.1",
63
68
  "@types/node": "^20.11.16",
64
69
  "typescript": "^5.3.3"
65
70
  },