@alanse/clickup-multi-mcp-server 1.0.0 → 1.0.2

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
@@ -1,9 +1,21 @@
1
- # ClickUp Multi-Workspace MCP Server
1
+ <p align="center">
2
+ <img src="assets/images/clickup-social.png" alt="ClickUp Multi-Workspace MCP Server" width="100%">
3
+ </p>
2
4
 
3
- [![GitHub Stars](https://img.shields.io/github/stars/da-okazaki/clickup-multi-mcp-server?style=flat&logo=github)](https://github.com/da-okazaki/clickup-multi-mcp-server/stargazers)
4
- [![NPM Version](https://img.shields.io/npm/v/@alanse/clickup-multi-mcp-server.svg?style=flat&logo=npm)](https://www.npmjs.com/package/@alanse/clickup-multi-mcp-server)
5
+ <h1 align="center">ClickUp Multi-Workspace MCP Server</h1>
5
6
 
6
- A Model Context Protocol (MCP) server for integrating **multiple ClickUp workspaces** with AI applications. This server allows AI agents to interact with tasks, spaces, lists, and folders across different ClickUp workspaces through a standardized protocol.
7
+ <p align="center">
8
+ <a href="https://github.com/da-okazaki/clickup-multi-mcp-server/stargazers">
9
+ <img src="https://img.shields.io/github/stars/da-okazaki/clickup-multi-mcp-server?style=flat&logo=github" alt="GitHub Stars">
10
+ </a>
11
+ <a href="https://www.npmjs.com/package/@alanse/clickup-multi-mcp-server">
12
+ <img src="https://img.shields.io/npm/v/@alanse/clickup-multi-mcp-server.svg?style=flat&logo=npm" alt="NPM Version">
13
+ </a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ A Model Context Protocol (MCP) server for integrating <strong>multiple ClickUp workspaces</strong> with AI applications. This server allows AI agents to interact with tasks, spaces, lists, and folders across different ClickUp workspaces through a standardized protocol.
18
+ </p>
7
19
 
8
20
  ## 🎯 Key Feature: Multi-Workspace Support
9
21
 
@@ -22,6 +34,27 @@ Based on the excellent [clickup-mcp-server](https://github.com/taazkareem/clicku
22
34
 
23
35
  ## Quick Start
24
36
 
37
+ ### Claude Code CLI Setup
38
+
39
+ The easiest way to add this MCP server to Claude Code:
40
+
41
+ **Single Workspace:**
42
+ ```bash
43
+ claude mcp add clickup \
44
+ -e CLICKUP_API_KEY=your_api_key_here \
45
+ -e CLICKUP_TEAM_ID=your_team_id_here \
46
+ -- npx -y @alanse/clickup-multi-mcp-server@latest
47
+ ```
48
+
49
+ **Multiple Workspaces:**
50
+ ```bash
51
+ claude mcp add clickup \
52
+ -e CLICKUP_WORKSPACES='{"default":"work","workspaces":{"work":{"token":"pk_xxx_work","teamId":"123456"},"personal":{"token":"pk_xxx_personal","teamId":"789012"}}}' \
53
+ -- npx -y @alanse/clickup-multi-mcp-server@latest
54
+ ```
55
+
56
+ ### Manual Configuration
57
+
25
58
  ### Single Workspace (Backwards Compatible)
26
59
 
27
60
  The traditional single workspace setup still works exactly as before:
@@ -145,17 +178,77 @@ await getTasks({ workspace: "personal", list_id: "987654321" });
145
178
  await getWorkspaceHierarchy({ workspace: "work" });
146
179
  ```
147
180
 
181
+ ## Local Development Setup
182
+
183
+ For local development or when running the server directly from source code:
184
+
185
+ ### 1. Clone and Install
186
+
187
+ ```bash
188
+ git clone https://github.com/da-okazaki/clickup-multi-mcp-server.git
189
+ cd clickup-multi-mcp-server
190
+ npm install
191
+ ```
192
+
193
+ ### 2. Configure Environment Variables
194
+
195
+ Copy `.env.example` to `.env` and configure your ClickUp credentials:
196
+
197
+ ```bash
198
+ cp .env.example .env
199
+ ```
200
+
201
+ Edit `.env` file:
202
+
203
+ ```bash
204
+ # Multi-workspace configuration
205
+ CLICKUP_WORKSPACES={"default":"alanse","workspaces":{"alanse":{"token":"pk_YOUR_TOKEN_1","teamId":"YOUR_TEAM_ID_1","description":"Alanse workspace"},"potz":{"token":"pk_YOUR_TOKEN_2","teamId":"YOUR_TEAM_ID_2","description":"Potz workspace"}}}
206
+
207
+ # Or use single workspace (legacy)
208
+ # CLICKUP_API_KEY=your_api_key_here
209
+ # CLICKUP_TEAM_ID=your_team_id_here
210
+ ```
211
+
212
+ **Note**: The `.env` file is automatically loaded when the server starts. Environment variables in `.env` are automatically picked up without needing to pass them via command line.
213
+
214
+ ### 3. Build and Run
215
+
216
+ ```bash
217
+ # Build the project
218
+ npm run build
219
+
220
+ # Run locally
221
+ node build/index.js
222
+ ```
223
+
224
+ ### 4. Configure Claude Code for Local Development
225
+
226
+ If you want to use your local build with Claude Code, update `~/.claude.json`:
227
+
228
+ ```json
229
+ {
230
+ "mcpServers": {
231
+ "clickup": {
232
+ "type": "stdio",
233
+ "command": "node",
234
+ "args": ["/absolute/path/to/clickup-multi-mcp-server/build/index.js"]
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ **Important**: When using local build with Claude Code:
241
+ - Environment variables are loaded from `.env` file in the project root
242
+ - No need to specify `env` in `~/.claude.json` (unless you want to override `.env` values)
243
+ - Rebuild after making changes: `npm run build`
244
+
148
245
  ## Smithery Installation (Quick Start)
149
246
 
150
247
  [![smithery badge](https://smithery.ai/badge/@taazkareem/clickup-mcp-server)](https://smithery.ai/server/@TaazKareem/clickup-mcp-server)
151
248
 
152
249
  The server is hosted on [Smithery](https://smithery.ai/server/@taazkareem/clickup-mcp-server). There, you can preview the available tools or copy the commands to run on your specific client app.
153
250
 
154
- ## NPX Installation
155
251
 
156
- [![NPM Version](https://img.shields.io/npm/v/@taazkareem/clickup-mcp-server.svg?style=flat&logo=npm)](https://www.npmjs.com/package/@taazkareem/clickup-mcp-server)
157
- [![Dependency Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen)](https://github.com/TaazKareem/clickup-mcp-server/blob/main/package.json)
158
- [![NPM Downloads](https://img.shields.io/npm/dm/@taazkareem/clickup-mcp-server.svg?style=flat&logo=npm)](https://npmcharts.com/compare/@taazkareem/clickup-mcp-server?minimal=true)
159
252
 
160
253
  Add this entry to your client's MCP settings JSON file:
161
254
 
@@ -335,11 +428,12 @@ npm run sse-client
335
428
  | ⚡ **Integration Features** | 🏗️ **Architecture & Performance** |
336
429
  | • 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 |
337
430
 
338
- ## Available Tools (36 Total)
431
+ ## Available Tools (42 Total)
339
432
 
340
433
  | Tool | Description | Required Parameters |
341
434
  | ------------------------------------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
342
435
  | [get_workspace_hierarchy](docs/user-guide.md#workspace-navigation) | Get workspace structure | None |
436
+ | [get_available_workspaces](docs/user-guide.md#workspace-navigation) | Get all available workspaces | None |
343
437
  | [create_task](docs/user-guide.md#task-management) | Create a task | `name`, (`listId`/`listName`) |
344
438
  | [create_bulk_tasks](docs/user-guide.md#task-management) | Create multiple tasks | `tasks[]` |
345
439
  | [update_task](docs/user-guide.md#task-management) | Modify task | `taskId`/`taskName` |
@@ -355,6 +449,11 @@ npm run sse-client
355
449
  | [move_task](docs/user-guide.md#task-management) | Move task | `taskId`/`taskName`, `listId`/`listName` |
356
450
  | [move_bulk_tasks](docs/user-guide.md#task-management) | Move multiple tasks | `tasks[]` with IDs or names, target list |
357
451
  | [duplicate_task](docs/user-guide.md#task-management) | Copy task | `taskId`/`taskName`, `listId`/`listName` |
452
+ | [merge_task](docs/user-guide.md#task-management) | Merge two tasks | `taskId`, `mergeFromId` |
453
+ | [get_task_time_in_status](docs/user-guide.md#task-management) | Get time in status for a task | `taskId`/`taskName` |
454
+ | [get_bulk_tasks_time_in_status](docs/user-guide.md#task-management)| Get bulk time in status | `taskIds[]` |
455
+ | [add_task_to_list](docs/user-guide.md#list-management) | Add task to additional list | `listId`, `taskId` |
456
+ | [remove_task_from_list](docs/user-guide.md#list-management) | Remove task from list | `listId`, `taskId` |
358
457
  | [create_list](docs/user-guide.md#list-management) | Create list in space | `name`, `spaceId`/`spaceName` |
359
458
  | [create_folder](docs/user-guide.md#folder-management) | Create folder | `name`, `spaceId`/`spaceName` |
360
459
  | [create_list_in_folder](docs/user-guide.md#list-management) | Create list in folder | `name`, `folderId`/`folderName` |
@@ -436,35 +535,9 @@ The server provides clear error messages for:
436
535
  The `LOG_LEVEL` environment variable can be specified to control the verbosity of server logs. Valid values are `trace`, `debug`, `info`, `warn`, and `error` (default).
437
536
  This can be also be specified on the command line as, e.g. `--env LOG_LEVEL=info`.
438
537
 
439
- ## Support the Developer
440
-
441
- When using this server, you may occasionally see a small sponsor message with a link to this repository included in tool responses. I hope you can support the project!
442
- If you find this project useful, please consider supporting:
443
-
444
- [![Sponsor TaazKareem](https://img.shields.io/badge/Sponsor-TaazKareem-orange?logo=github)](https://github.com/sponsors/TaazKareem)
445
-
446
- <a href="https://buymeacoffee.com/taazkareem">
447
- <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" width="200" alt="Buy Me A Coffee">
448
- </a>
449
-
450
- ## Acknowledgements
451
-
452
- Special thanks to [ClickUp](https://clickup.com) for their excellent API and services that make this integration possible.
453
-
454
- ## Contributing
455
-
456
- Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
457
538
 
458
539
  ## License
459
540
 
460
541
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
461
542
 
462
543
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
463
-
464
- ## Disclaimer
465
-
466
- This software makes use of third-party APIs and may reference trademarks
467
- or brands owned by third parties. The use of such APIs or references does not imply
468
- any affiliation with or endorsement by the respective companies. All trademarks and
469
- brand names are the property of their respective owners. This project is an independent
470
- work and is not officially associated with or sponsored by any third-party company mentioned.
package/build/config.js CHANGED
@@ -21,6 +21,9 @@
21
21
  * - SSE_PORT: Port for SSE server (default: 3000)
22
22
  * - ENABLE_STDIO: Enable STDIO transport (default: true)
23
23
  */
24
+ // Load environment variables from .env file
25
+ import dotenv from 'dotenv';
26
+ dotenv.config();
24
27
  // Parse any command line environment arguments
25
28
  const args = process.argv.slice(2);
26
29
  const envArgs = {};
@@ -125,7 +128,6 @@ const configuration = {
125
128
  clickupApiKey: envArgs.clickupApiKey || process.env.CLICKUP_API_KEY || '',
126
129
  clickupTeamId: envArgs.clickupTeamId || process.env.CLICKUP_TEAM_ID || '',
127
130
  clickupWorkspaces: parseWorkspaces(),
128
- enableSponsorMessage: process.env.ENABLE_SPONSOR_MESSAGE !== 'false',
129
131
  documentSupport: envArgs.documentSupport || process.env.DOCUMENT_SUPPORT || process.env.DOCUMENT_MODULE || process.env.DOCUMENT_MODEL || 'false',
130
132
  logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL),
131
133
  disabledTools: ((envArgs.disabledTools || process.env.DISABLED_TOOLS || process.env.DISABLED_COMMANDS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
package/build/server.js CHANGED
@@ -7,8 +7,8 @@
7
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
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";
10
+ import { workspaceHierarchyTool, handleGetWorkspaceHierarchy, availableWorkspacesTool, handleGetAvailableWorkspaces } 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, mergeTaskTool, getTimeInStatusTool, getBulkTimeInStatusTool, addTaskToListTool, removeTaskFromListTool, handleCreateTask, handleUpdateTask, handleMoveTask, handleDuplicateTask, handleDeleteTask, handleGetTaskComments, handleCreateTaskComment, handleCreateBulkTasks, handleUpdateBulkTasks, handleMoveBulkTasks, handleDeleteBulkTasks, handleGetTask, handleAttachTaskFile, handleGetWorkspaceTasks, handleGetTaskTimeEntries, handleStartTimeTracking, handleStopTimeTracking, handleAddTimeEntry, handleDeleteTimeEntry, handleGetCurrentTimeEntry, mergeTaskHandler, getTimeInStatusHandler, getBulkTimeInStatusHandler, addTaskToListHandler, removeTaskFromListHandler } from "./tools/task/index.js";
12
12
  import { createListTool, handleCreateList, createListInFolderTool, handleCreateListInFolder, getListTool, handleGetList, updateListTool, handleUpdateList, deleteListTool, handleDeleteList } from "./tools/list.js";
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";
@@ -81,6 +81,7 @@ export function configureServer() {
81
81
  // Collect all tool definitions
82
82
  const allTools = [
83
83
  workspaceHierarchyTool,
84
+ availableWorkspacesTool,
84
85
  createTaskTool,
85
86
  getTaskTool,
86
87
  updateTaskTool,
@@ -101,6 +102,11 @@ export function configureServer() {
101
102
  addTimeEntryTool,
102
103
  deleteTimeEntryTool,
103
104
  getCurrentTimeEntryTool,
105
+ mergeTaskTool,
106
+ getTimeInStatusTool,
107
+ getBulkTimeInStatusTool,
108
+ addTaskToListTool,
109
+ removeTaskFromListTool,
104
110
  createListTool,
105
111
  createListInFolderTool,
106
112
  getListTool,
@@ -156,7 +162,9 @@ export function configureServer() {
156
162
  // Handle tool calls by routing to the appropriate handler
157
163
  switch (name) {
158
164
  case "get_workspace_hierarchy":
159
- return handleGetWorkspaceHierarchy();
165
+ return handleGetWorkspaceHierarchy(params);
166
+ case "get_available_workspaces":
167
+ return handleGetAvailableWorkspaces();
160
168
  case "create_task":
161
169
  return handleCreateTask(params);
162
170
  case "update_task":
@@ -221,6 +229,16 @@ export function configureServer() {
221
229
  return handleDeleteTimeEntry(params);
222
230
  case "get_current_time_entry":
223
231
  return handleGetCurrentTimeEntry(params);
232
+ case "merge_task":
233
+ return mergeTaskHandler(params);
234
+ case "get_task_time_in_status":
235
+ return getTimeInStatusHandler(params);
236
+ case "get_bulk_tasks_time_in_status":
237
+ return getBulkTimeInStatusHandler(params);
238
+ case "add_task_to_list":
239
+ return addTaskToListHandler(params);
240
+ case "remove_task_from_list":
241
+ return removeTaskFromListHandler(params);
224
242
  case "create_document":
225
243
  return handleCreateDocument(params);
226
244
  case "get_document":
@@ -0,0 +1 @@
1
+ Logging initialized to /Users/daichiokazaki/Documents/project/alanse/clickup-multi-mcp-server/build/server.log
@@ -601,4 +601,103 @@ export class TaskServiceCore extends BaseClickUpService {
601
601
  });
602
602
  this.logger.debug('Cached task name to ID mapping', { taskName, taskId, listId });
603
603
  }
604
+ /**
605
+ * Merge two tasks together
606
+ * @param taskId ID of the task to merge into (destination)
607
+ * @param data Merge configuration including merge_from_id
608
+ * @returns The merged task
609
+ */
610
+ async mergeTask(taskId, data) {
611
+ try {
612
+ this.logOperation('mergeTask', { taskId, ...data });
613
+ const path = `/task/${taskId}/merge`;
614
+ this.traceRequest('POST', path, data);
615
+ return await this.makeRequest(async () => {
616
+ const response = await this.client.post(path, data);
617
+ return response.data;
618
+ });
619
+ }
620
+ catch (error) {
621
+ throw this.handleError(error, `Failed to merge task ${taskId}`);
622
+ }
623
+ }
624
+ /**
625
+ * Get time in status for a task
626
+ * @param taskId ID of the task
627
+ * @returns Time in status data
628
+ */
629
+ async getTimeInStatus(taskId) {
630
+ try {
631
+ this.logOperation('getTimeInStatus', { taskId });
632
+ const path = `/task/${taskId}/time_in_status`;
633
+ this.traceRequest('GET', path);
634
+ return await this.makeRequest(async () => {
635
+ const response = await this.client.get(path);
636
+ return response.data;
637
+ });
638
+ }
639
+ catch (error) {
640
+ throw this.handleError(error, `Failed to get time in status for task ${taskId}`);
641
+ }
642
+ }
643
+ /**
644
+ * Get time in status for multiple tasks
645
+ * @param taskIds Array of task IDs
646
+ * @returns Bulk time in status data
647
+ */
648
+ async getBulkTimeInStatus(taskIds) {
649
+ try {
650
+ this.logOperation('getBulkTimeInStatus', { taskIds, count: taskIds.length });
651
+ // Convert array to query string: ?task_ids=id1&task_ids=id2
652
+ const queryParams = taskIds.map(id => `task_ids=${id}`).join('&');
653
+ const path = `/task/bulk_time_in_status/task_ids?${queryParams}`;
654
+ this.traceRequest('GET', path);
655
+ return await this.makeRequest(async () => {
656
+ const response = await this.client.get(path);
657
+ return response.data;
658
+ });
659
+ }
660
+ catch (error) {
661
+ throw this.handleError(error, `Failed to get bulk time in status`);
662
+ }
663
+ }
664
+ /**
665
+ * Add a task to an additional list (multiple list membership)
666
+ * @param listId ID of the list to add the task to
667
+ * @param taskId ID of the task to add
668
+ * @returns The updated task
669
+ */
670
+ async addTaskToList(listId, taskId) {
671
+ try {
672
+ this.logOperation('addTaskToList', { listId, taskId });
673
+ const path = `/list/${listId}/task/${taskId}`;
674
+ this.traceRequest('POST', path);
675
+ return await this.makeRequest(async () => {
676
+ const response = await this.client.post(path);
677
+ return response.data;
678
+ });
679
+ }
680
+ catch (error) {
681
+ throw this.handleError(error, `Failed to add task ${taskId} to list ${listId}`);
682
+ }
683
+ }
684
+ /**
685
+ * Remove a task from a list
686
+ * @param listId ID of the list to remove the task from
687
+ * @param taskId ID of the task to remove
688
+ * @returns Success indicator
689
+ */
690
+ async removeTaskFromList(listId, taskId) {
691
+ try {
692
+ this.logOperation('removeTaskFromList', { listId, taskId });
693
+ const path = `/list/${listId}/task/${taskId}`;
694
+ this.traceRequest('DELETE', path);
695
+ await this.makeRequest(async () => {
696
+ await this.client.delete(path);
697
+ });
698
+ }
699
+ catch (error) {
700
+ throw this.handleError(error, `Failed to remove task ${taskId} from list ${listId}`);
701
+ }
702
+ }
604
703
  }
@@ -21,12 +21,15 @@ const workspaceServicesMap = new Map();
21
21
  */
22
22
  export function getClickUpServices(workspaceId) {
23
23
  const wsId = workspaceId || getDefaultWorkspace();
24
+ console.error('[DEBUG] getClickUpServices called with workspaceId:', workspaceId);
25
+ console.error('[DEBUG] Resolved workspace ID:', wsId);
24
26
  // Check if services already exist for this workspace
25
27
  let services = workspaceServicesMap.get(wsId);
26
28
  if (!services) {
27
29
  logger.info(`Creating ClickUp services for workspace: ${wsId}`);
28
30
  // Get workspace configuration
29
31
  const workspaceConfig = getWorkspaceConfig(wsId);
32
+ console.error('[DEBUG] Workspace config:', JSON.stringify(workspaceConfig));
30
33
  // Create the services instance
31
34
  services = createClickUpServices({
32
35
  apiKey: workspaceConfig.token,
@@ -15,6 +15,7 @@ import { validateTaskIdentification, validateListIdentification, validateTaskUpd
15
15
  import { handleResolveAssignees } from '../member.js';
16
16
  import { isNameMatch } from '../../utils/resolver-utils.js';
17
17
  import { Logger } from '../../logger.js';
18
+ import { sponsorService } from '../../utils/sponsor-service.js';
18
19
  // Use default workspace services for backwards compatibility
19
20
  const defaultServices = getClickUpServices();
20
21
  const { task: taskService, list: listService, workspace: workspaceService } = defaultServices;
@@ -917,3 +918,88 @@ export async function deleteTaskHandler(params) {
917
918
  await taskService.deleteTask(taskId);
918
919
  return true;
919
920
  }
921
+ /**
922
+ * Handler for merging two tasks
923
+ */
924
+ export async function mergeTaskHandler(params) {
925
+ try {
926
+ const { taskId, mergeFromId } = params;
927
+ if (!taskId || !mergeFromId) {
928
+ throw new Error('taskId and mergeFromId are required');
929
+ }
930
+ const result = await taskService.mergeTask(taskId, {
931
+ merge_from_id: mergeFromId
932
+ });
933
+ return sponsorService.createResponse(result, true);
934
+ }
935
+ catch (error) {
936
+ return sponsorService.createErrorResponse(error, params);
937
+ }
938
+ }
939
+ /**
940
+ * Handler for getting time in status for a task
941
+ */
942
+ export async function getTimeInStatusHandler(params) {
943
+ try {
944
+ const { taskId } = params;
945
+ if (!taskId) {
946
+ throw new Error('taskId is required');
947
+ }
948
+ const result = await taskService.getTimeInStatus(taskId);
949
+ return sponsorService.createResponse(result, true);
950
+ }
951
+ catch (error) {
952
+ return sponsorService.createErrorResponse(error, params);
953
+ }
954
+ }
955
+ /**
956
+ * Handler for getting time in status for multiple tasks
957
+ */
958
+ export async function getBulkTimeInStatusHandler(params) {
959
+ try {
960
+ const { taskIds } = params;
961
+ if (!taskIds || !Array.isArray(taskIds) || taskIds.length === 0) {
962
+ throw new Error('taskIds array is required and must not be empty');
963
+ }
964
+ const result = await taskService.getBulkTimeInStatus(taskIds);
965
+ return sponsorService.createResponse(result, true);
966
+ }
967
+ catch (error) {
968
+ return sponsorService.createErrorResponse(error, params);
969
+ }
970
+ }
971
+ /**
972
+ * Handler for adding a task to an additional list
973
+ */
974
+ export async function addTaskToListHandler(params) {
975
+ try {
976
+ const { listId, taskId } = params;
977
+ if (!listId || !taskId) {
978
+ throw new Error('listId and taskId are required');
979
+ }
980
+ const result = await taskService.addTaskToList(listId, taskId);
981
+ return sponsorService.createResponse(result, true);
982
+ }
983
+ catch (error) {
984
+ return sponsorService.createErrorResponse(error, params);
985
+ }
986
+ }
987
+ /**
988
+ * Handler for removing a task from a list
989
+ */
990
+ export async function removeTaskFromListHandler(params) {
991
+ try {
992
+ const { listId, taskId } = params;
993
+ if (!listId || !taskId) {
994
+ throw new Error('listId and taskId are required');
995
+ }
996
+ await taskService.removeTaskFromList(listId, taskId);
997
+ return sponsorService.createResponse({
998
+ success: true,
999
+ message: `Task ${taskId} removed from list ${listId}`
1000
+ }, true);
1001
+ }
1002
+ catch (error) {
1003
+ return sponsorService.createErrorResponse(error, params);
1004
+ }
1005
+ }
@@ -9,7 +9,7 @@
9
9
  // Re-export from main module
10
10
  export * from './main.js';
11
11
  // Re-export single task operation tools
12
- export { createTaskTool, getTaskTool, getTasksTool, updateTaskTool, moveTaskTool, duplicateTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool } from './single-operations.js';
12
+ export { createTaskTool, getTaskTool, getTasksTool, updateTaskTool, moveTaskTool, duplicateTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool, mergeTaskTool, getTimeInStatusTool, getBulkTimeInStatusTool, addTaskToListTool, removeTaskFromListTool } from './single-operations.js';
13
13
  // Re-export bulk task operation tools
14
14
  export { createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool } from './bulk-operations.js';
15
15
  // Re-export workspace task operation tools
@@ -21,7 +21,7 @@ export { attachTaskFileTool, handleAttachTaskFile } from './attachments.js';
21
21
  // Re-export handlers
22
22
  export {
23
23
  // Single task operation handlers
24
- createTaskHandler, getTaskHandler, getTasksHandler, updateTaskHandler, moveTaskHandler, duplicateTaskHandler, deleteTaskHandler, getTaskCommentsHandler, createTaskCommentHandler,
24
+ createTaskHandler, getTaskHandler, getTasksHandler, updateTaskHandler, moveTaskHandler, duplicateTaskHandler, deleteTaskHandler, getTaskCommentsHandler, createTaskCommentHandler, mergeTaskHandler, getTimeInStatusHandler, getBulkTimeInStatusHandler, addTaskToListHandler, removeTaskFromListHandler,
25
25
  // Bulk task operation handlers
26
26
  createBulkTasksHandler, updateBulkTasksHandler, moveBulkTasksHandler, deleteBulkTasksHandler,
27
27
  // Team task operation handlers
@@ -467,3 +467,142 @@ export const deleteTaskTool = {
467
467
  }
468
468
  }
469
469
  };
470
+ /**
471
+ * Tool definition for merging two tasks
472
+ */
473
+ export const mergeTaskTool = {
474
+ name: "merge_task",
475
+ description: `Merges two ClickUp tasks together. The merge_from task will be merged into the destination task.
476
+
477
+ Example usage:
478
+ {
479
+ "taskId": "abc123", // Destination task ID (merge into this)
480
+ "mergeFromId": "xyz789" // Source task ID (merge from this)
481
+ }`,
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ taskId: {
486
+ type: "string",
487
+ description: "REQUIRED: ID of the destination task (merge into this task)"
488
+ },
489
+ mergeFromId: {
490
+ type: "string",
491
+ description: "REQUIRED: ID of the source task (merge from this task)"
492
+ }
493
+ },
494
+ required: ["taskId", "mergeFromId"]
495
+ }
496
+ };
497
+ /**
498
+ * Tool definition for getting time in status for a task
499
+ */
500
+ export const getTimeInStatusTool = {
501
+ name: "get_task_time_in_status",
502
+ description: `Gets the time tracking information for how long a task has been in each status.
503
+
504
+ Example usage:
505
+ {
506
+ "taskId": "abc123"
507
+ }
508
+
509
+ Returns:
510
+ - current_status: Current status with time spent
511
+ - status_history: Array of all status changes with timestamps`,
512
+ inputSchema: {
513
+ type: "object",
514
+ properties: {
515
+ taskId: {
516
+ type: "string",
517
+ description: "REQUIRED: ID of the task to get time in status for"
518
+ }
519
+ },
520
+ required: ["taskId"]
521
+ }
522
+ };
523
+ /**
524
+ * Tool definition for getting time in status for multiple tasks
525
+ */
526
+ export const getBulkTimeInStatusTool = {
527
+ name: "get_bulk_tasks_time_in_status",
528
+ description: `Gets the time in status information for multiple tasks at once. More efficient than calling get_task_time_in_status multiple times.
529
+
530
+ Example usage:
531
+ {
532
+ "taskIds": ["abc123", "xyz789", "def456"]
533
+ }
534
+
535
+ Returns an object with task IDs as keys and their time in status data as values.`,
536
+ inputSchema: {
537
+ type: "object",
538
+ properties: {
539
+ taskIds: {
540
+ type: "array",
541
+ description: "REQUIRED: Array of task IDs to get time in status for",
542
+ items: {
543
+ type: "string"
544
+ },
545
+ minItems: 1
546
+ }
547
+ },
548
+ required: ["taskIds"]
549
+ }
550
+ };
551
+ /**
552
+ * Tool definition for adding a task to an additional list
553
+ */
554
+ export const addTaskToListTool = {
555
+ name: "add_task_to_list",
556
+ description: `Adds a task to an additional list, enabling multiple list membership for the task.
557
+
558
+ Example usage:
559
+ {
560
+ "listId": "12345",
561
+ "taskId": "abc123"
562
+ }
563
+
564
+ Note: This does NOT move the task. The task will belong to multiple lists after this operation.`,
565
+ inputSchema: {
566
+ type: "object",
567
+ properties: {
568
+ listId: {
569
+ type: "string",
570
+ description: "REQUIRED: ID of the list to add the task to"
571
+ },
572
+ taskId: {
573
+ type: "string",
574
+ description: "REQUIRED: ID of the task to add to the list"
575
+ }
576
+ },
577
+ required: ["listId", "taskId"]
578
+ }
579
+ };
580
+ /**
581
+ * Tool definition for removing a task from a list
582
+ */
583
+ export const removeTaskFromListTool = {
584
+ name: "remove_task_from_list",
585
+ description: `Removes a task from a specific list. If the task belongs to multiple lists, it will only be removed from the specified list.
586
+
587
+ Example usage:
588
+ {
589
+ "listId": "12345",
590
+ "taskId": "abc123"
591
+ }
592
+
593
+ Warning: If this is the task's only list, the task may be deleted or moved to an archive depending on ClickUp settings.`,
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ listId: {
598
+ type: "string",
599
+ description: "REQUIRED: ID of the list to remove the task from"
600
+ },
601
+ taskId: {
602
+ type: "string",
603
+ description: "REQUIRED: ID of the task to remove from the list"
604
+ }
605
+ },
606
+ required: ["listId", "taskId"]
607
+ }
608
+ };
@@ -25,7 +25,10 @@ export const workspaceParameter = {
25
25
  */
26
26
  export function getServicesForWorkspace(params) {
27
27
  try {
28
- return getClickUpServices(params.workspace);
28
+ console.error('[DEBUG] getServicesForWorkspace called with workspace:', params.workspace);
29
+ const services = getClickUpServices(params.workspace);
30
+ console.error('[DEBUG] Retrieved services for workspace:', params.workspace || 'default');
31
+ return services;
29
32
  }
30
33
  catch (error) {
31
34
  // Enhance error message with available workspaces
@@ -30,6 +30,9 @@ export const workspaceHierarchyTool = {
30
30
  */
31
31
  export async function handleGetWorkspaceHierarchy(params = {}) {
32
32
  try {
33
+ // DEBUG: Log received parameters
34
+ console.error('[DEBUG] handleGetWorkspaceHierarchy called with params:', JSON.stringify(params));
35
+ console.error('[DEBUG] params.workspace =', params.workspace);
33
36
  // Get services for the specified workspace
34
37
  const services = getServicesForWorkspace(params);
35
38
  const { workspace: workspaceService } = services;
@@ -44,6 +47,49 @@ export async function handleGetWorkspaceHierarchy(params = {}) {
44
47
  return sponsorService.createErrorResponse(`Error getting workspace hierarchy: ${error.message}`);
45
48
  }
46
49
  }
50
+ /**
51
+ * Tool definition for retrieving available workspaces
52
+ */
53
+ export const availableWorkspacesTool = {
54
+ name: 'get_available_workspaces',
55
+ description: `Gets list of all available workspaces configured in the ClickUp MCP server. Returns workspace identifiers, default workspace, and descriptions if available.`,
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {}
59
+ }
60
+ };
61
+ /**
62
+ * Handler for the get_available_workspaces tool
63
+ */
64
+ export async function handleGetAvailableWorkspaces() {
65
+ try {
66
+ // Import config functions
67
+ const { getAvailableWorkspaces, getDefaultWorkspace, getWorkspaceConfig } = await import('../config.js');
68
+ // Get list of available workspace identifiers
69
+ const workspaceIds = getAvailableWorkspaces();
70
+ const defaultWorkspace = getDefaultWorkspace();
71
+ // Get detailed information for each workspace
72
+ const workspaces = workspaceIds.map(id => {
73
+ const config = getWorkspaceConfig(id);
74
+ return {
75
+ id,
76
+ teamId: config.teamId,
77
+ description: config.description || (id === 'default' ? 'Default workspace' : undefined),
78
+ isDefault: id === defaultWorkspace
79
+ };
80
+ });
81
+ const result = {
82
+ default: defaultWorkspace,
83
+ workspaces,
84
+ count: workspaces.length
85
+ };
86
+ // Use sponsor service to create the response
87
+ return sponsorService.createResponse(result, true);
88
+ }
89
+ catch (error) {
90
+ return sponsorService.createErrorResponse(`Error getting available workspaces: ${error.message}`);
91
+ }
92
+ }
47
93
  /**
48
94
  * Format the hierarchy as a tree string
49
95
  */
@@ -1,37 +1,26 @@
1
1
  /**
2
2
  * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-FileCopyrightText: © 2025 Daichi Okazaki (Alanse)
3
4
  * SPDX-License-Identifier: MIT
4
5
  *
5
- * Sponsor Service Module
6
+ * Response Service Module
6
7
  *
7
- * Provides configuration and utilities for sponsorship functionality
8
+ * Provides utilities for creating standardized MCP responses
8
9
  */
9
10
  import { Logger } from '../logger.js';
10
- import config from '../config.js';
11
11
  // Create logger instance for this module
12
- const logger = new Logger('SponsorService');
12
+ const logger = new Logger('ResponseService');
13
13
  /**
14
- * SponsorService - Provides sponsorship configuration and message handling
14
+ * ResponseService - Provides standardized response formatting
15
15
  */
16
- export class SponsorService {
16
+ export class ResponseService {
17
17
  constructor() {
18
- this.sponsorUrl = 'https://github.com/sponsors/taazkareem';
19
- this.isEnabled = config.enableSponsorMessage;
20
- logger.info('SponsorService initialized', { enabled: this.isEnabled });
18
+ logger.info('ResponseService initialized');
21
19
  }
22
20
  /**
23
- * Get sponsor information (for documentation/reference purposes)
21
+ * Creates a standardized response
24
22
  */
25
- getSponsorInfo() {
26
- return {
27
- isEnabled: this.isEnabled,
28
- url: this.sponsorUrl
29
- };
30
- }
31
- /**
32
- * Creates a response with optional sponsorship message
33
- */
34
- createResponse(data, includeSponsorMessage = false) {
23
+ createResponse(data, _includeSponsorMessage = false) {
35
24
  const content = [];
36
25
  // Special handling for workspace hierarchy which contains a preformatted tree
37
26
  if (data && typeof data === 'object' && 'hierarchy' in data && typeof data.hierarchy === 'string') {
@@ -55,13 +44,6 @@ export class SponsorService {
55
44
  text: JSON.stringify(data, null, 2)
56
45
  });
57
46
  }
58
- // Then add sponsorship message if enabled
59
- if (this.isEnabled && includeSponsorMessage) {
60
- content.push({
61
- type: "text",
62
- text: `\n♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
63
- });
64
- }
65
47
  return { content };
66
48
  }
67
49
  /**
@@ -74,7 +56,7 @@ export class SponsorService {
74
56
  });
75
57
  }
76
58
  /**
77
- * Creates a bulk operation response with sponsorship message
59
+ * Creates a bulk operation response
78
60
  */
79
61
  createBulkResponse(result) {
80
62
  return this.createResponse({
@@ -86,8 +68,9 @@ export class SponsorService {
86
68
  id: failure.item?.id || failure.item,
87
69
  error: failure.error.message
88
70
  }))
89
- }, true); // Always include sponsor message for bulk operations
71
+ });
90
72
  }
91
73
  }
92
- // Export a singleton instance
93
- export const sponsorService = new SponsorService();
74
+ // Export a singleton instance for backwards compatibility
75
+ // (keeping the name 'sponsorService' to avoid changing all imports)
76
+ export const sponsorService = new ResponseService();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alanse/clickup-multi-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "ClickUp MCP Server with Multi-Workspace Support - Integrate multiple ClickUp workspaces with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",