@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 +105 -10
- package/build/config.js +30 -0
- package/build/index.js +36 -20
- package/build/schemas/member.js +13 -0
- package/build/server.js +13 -3
- package/build/services/clickup/workspace.js +28 -0
- package/build/sse_server.js +113 -0
- package/build/tools/index.js +1 -0
- package/build/tools/member.js +108 -0
- package/build/tools/task/bulk-operations.js +22 -2
- package/build/tools/task/handlers.js +42 -3
- package/build/tools/task/main.js +14 -0
- package/build/tools/task/single-operations.js +26 -2
- package/build/tools/task/utilities.js +4 -8
- package/package.json +8 -3
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.
|
|
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
|
|
65
|
-
|
|
|
66
|
-
| • Create, update, and delete tasks
|
|
67
|
-
|
|
|
68
|
-
| • View time entries for tasks
|
|
69
|
-
|
|
|
70
|
-
| •
|
|
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
|
|
25
|
-
import { configureServer, server } from
|
|
26
|
-
import { info, error } from
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
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
|
+
}
|
package/build/tools/index.js
CHANGED
|
@@ -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) {
|
package/build/tools/task/main.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.11.3",
|
|
59
59
|
"axios": "^1.6.7",
|
|
60
|
-
"
|
|
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
|
},
|