#
tokens: 18757/50000 25/25 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── pr_agent.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.MD
├── src
│   ├── controllers
│   │   ├── assignee.controller.ts
│   │   ├── custom-field.controller.ts
│   │   ├── docs.controller.ts
│   │   ├── folder.controller.ts
│   │   ├── list.controller.ts
│   │   ├── space.controller.ts
│   │   └── task.controller.ts
│   ├── index.ts
│   ├── models
│   │   ├── schema.ts
│   │   └── types.ts
│   ├── services
│   │   ├── assignee.service.ts
│   │   ├── custom-field.service.ts
│   │   ├── docs.service.ts
│   │   ├── folder.service.ts
│   │   ├── list.service.ts
│   │   ├── space.service.ts
│   │   └── task.service.ts
│   └── utils
│       ├── defineTool.ts
│       └── errors.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | .env
2 | node_modules/
3 | dist/
4 | .vscode/
5 | .idea/
6 | notes.txt
7 | 
```

--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------

```markdown
  1 | # ClickUp MCP Integration
  2 | 
  3 | A Model Context Protocol server that provides seamless integration with ClickUp, allowing Large Language Models to interact with your ClickUp workspace tasks and data.
  4 | 
  5 | ## Available Tools
  6 | 
  7 | This MCP server provides the following tools for interacting with ClickUp:
  8 | 
  9 | ### Task Management
 10 | 
 11 | - **`clickup_create_task`**: Creates a new task in your ClickUp workspace
 12 | - **`clickup_get_task`**: Retrieves detailed information about a specific task using its ID
 13 | - **`clickup_get_task_by_custom_id`**: Retrieves task information using a custom ID
 14 | - **`clickup_update_task`**: Updates an existing task by its ID
 15 | - **`clickup_update_task_by_custom_id`**: Updates an existing task by its custom ID
 16 | - **`get_list_tasks`**: Gets all tasks from a list with optional filtering
 17 | 
 18 | ### Document Management
 19 | 
 20 | - **`clickup_search_docs`**: Searches for docs in a specific parent
 21 | - **`clickup_create_doc`**: Creates a new doc in ClickUp
 22 | - **`clickup_get_doc_pages`**: Gets all pages from a ClickUp doc
 23 | - **`clickup_get_page`**: Gets a specific page from a ClickUp doc
 24 | - **`clickup_create_page`**: Creates a new page in a ClickUp doc
 25 | - **`clickup_edit_page`**: Edits an existing page in a ClickUp doc
 26 | 
 27 | ### Custom Fields
 28 | 
 29 | - **`clickup_get_list_custom_fields`**: Gets all accessible custom fields for a list
 30 | - **`clickup_set_custom_field_value`**: Sets a value for a custom field on a task
 31 | - **`clickup_set_custom_field_value_by_custom_id`**: Sets a custom field value using the task's custom ID
 32 | 
 33 | ### Assignees
 34 | 
 35 | - **`get_list_assignees`**: Gets all members (potential assignees) of a list
 36 | 
 37 | ### Workspace Structure
 38 | 
 39 | - **`get_spaces`**: Gets all spaces in the workspace
 40 | - **`get_folders`**: Gets all folders in a space
 41 | - **`get_lists`**: Gets all lists in a folder
 42 | - **`create_list`**: Creates a new list in a folder
 43 | 
 44 | ## Build
 45 | 
 46 | Run:
 47 | 
 48 | ```bash
 49 | npm i
 50 | npm run build
 51 | npm run inspector
 52 | ```
 53 | 
 54 | Docker build:
 55 | 
 56 | ```bash
 57 | docker buildx build -t {your-docker-repository} --platform linux/amd64,linux/arm64 .
 58 | docker push {your-docker-repository}
 59 | ```
 60 | 
 61 | ## Setup
 62 | 
 63 | **1. Obtaining your ClickUp API Token:**
 64 | 
 65 | 1. Log in to your ClickUp account at [app.clickup.com](https://app.clickup.com)
 66 | 2. Navigate to your user settings by clicking your profile picture in the bottom-left corner
 67 | 3. Select "Settings"
 68 | 4. Click on "Apps" in the left sidebar
 69 | 5. Under "API Token", click "Generate" if you don't already have a token
 70 | 6. Copy the generated API token for use in the MCP server configuration
 71 | 
 72 | **2. Finding your Workspace ID:**
 73 | 
 74 | 1. Open ClickUp in your web browser
 75 | 2. Look at the URL when you're in your workspace
 76 | 3. The Workspace ID is the numeric value in the URL: `https://app.clickup.com/{workspace_id}/home`
 77 | 4. Copy this number for use in the MCP server configuration
 78 | 
 79 | **3. Install Docker:** https://docs.docker.com/engine/install/
 80 | 
 81 | **4a. Setup Cline MCP Server:**
 82 | 
 83 | - Open VSCode or Jetbrains IDEs and go to Cline.
 84 | - Go to MCP Servers → Installed → Configure MCP Servers.
 85 | - Add the following to your `cline_mcp_settings.json` inside the `mcpServers` key:
 86 | 
 87 | ```json
 88 | "clickup": {
 89 |   "command": "docker",
 90 |   "args": [
 91 |     "run",
 92 |     "-i",
 93 |     "--rm",
 94 |     "-e",
 95 |     "CLICKUP_API_TOKEN",
 96 |     "-e",
 97 |     "CLICKUP_WORKSPACE_ID",
 98 |     "your-docker-repository"
 99 |   ],
100 |   "env": {
101 |     "CLICKUP_API_TOKEN": "your-api-token",
102 |     "CLICKUP_WORKSPACE_ID": "your-workspace-id"
103 |   }
104 | }
105 | ```
106 | 
107 | **4b. Setup Claude Desktop MCP Server:**
108 | 
109 | - Use any editor to open the configuration file of Claude Desktop.
110 |   - Windows: `C:\Users\YourUsername\AppData\Roaming\Claude\claude_desktop_config.json`
111 |   - Mac: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
112 | - Add the following to your `claude_desktop_config.json` inside the `mcpServers` key:
113 | 
114 | ```json
115 | "clickup": {
116 |   "command": "docker",
117 |   "args": [
118 |     "run",
119 |     "-i",
120 |     "--rm",
121 |     "-e",
122 |     "CLICKUP_API_TOKEN",
123 |     "-e",
124 |     "CLICKUP_WORKSPACE_ID",
125 |     "your-docker-repository"
126 |   ],
127 |   "env": {
128 |     "CLICKUP_API_TOKEN": "your-api-token",
129 |     "CLICKUP_WORKSPACE_ID": "your-workspace-id"
130 |   }
131 | }
132 | ```
133 | 
134 | - Save the configuration file
135 | - Restart Claude Desktop to apply the changes
136 | 
137 | ### Troubleshooting
138 | 
139 | If you encounter issues with the MCP server:
140 | 
141 | 1. **Authentication Errors**:
142 | 
143 |    - Verify your API token is correct
144 |    - Ensure the API token has the necessary permissions for the operations you're attempting
145 |    - Check that your workspace ID is correct
146 | 
147 | 2. **Task Access Issues**:
148 | 
149 |    - Confirm you have access to the tasks you're trying to retrieve
150 |    - Verify the task IDs are correct and exist in your workspace
151 |    - Check if the tasks might be in an archived state
152 | 
153 | 3. **Connection Problems**:
154 | 
155 |    - Ensure your Docker service is running properly
156 |    - Check your network connection
157 |    - Verify the environment variables are correctly set in your MCP configuration
158 | 
159 | ## License
160 | 
161 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License.
162 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22.12-alpine AS builder
 2 | 
 3 | COPY . /app
 4 | WORKDIR /app
 5 | 
 6 | # Install dependencies and build TypeScript code
 7 | RUN npm ci && npm run build
 8 | 
 9 | # Set environment variables
10 | ENV NODE_ENV=production
11 | # ENV CLICKUP_WORKSPACE_ID=123456789
12 | 
13 | # Run the server
14 | CMD ["node", "dist/index.js"]
15 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "CommonJS",
 5 |     "moduleResolution": "Node",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "./dist",
12 |     "declaration": true,
13 |     "declarationDir": "./dist",
14 |     "sourceMap": true,
15 |     "rootDir": "src"
16 |   },
17 |   "include": ["src/**/*.ts"],
18 |   "exclude": ["node_modules", "dist"]
19 | }
20 | 
```

--------------------------------------------------------------------------------
/src/controllers/space.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dottenv from "dotenv";
 2 | import { defineTool } from "../utils/defineTool";
 3 | import SpaceService from "../services/space.service";
 4 | 
 5 | dottenv.config();
 6 | const apiToken = process.env.CLICKUP_API_TOKEN;
 7 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 8 | 
 9 | if (!apiToken || !workspaceId) {
10 |   console.error(
11 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
12 |   );
13 |   process.exit(1);
14 | }
15 | 
16 | const spaceService = new SpaceService(apiToken, workspaceId);
17 | 
18 | const getSpacesTool = defineTool((z) => ({
19 |   name: "get_spaces",
20 |   description: "Get all spaces in the workspace",
21 |   inputSchema: {},
22 |   handler: async (input) => {
23 |     const response = await spaceService.getSpaces();
24 |     return {
25 |       content: [{ type: "text", text: JSON.stringify(response) }],
26 |     };
27 |   },
28 | }));
29 | 
30 | export { getSpacesTool };
31 | 
```

--------------------------------------------------------------------------------
/src/services/space.service.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ClickUpUser } from "../models/types";
 2 | 
 3 | const BASE_URL = "https://api.clickup.com/api/v2";
 4 | 
 5 | export class SpaceService {
 6 |   private readonly headers: { Authorization: string; "Content-Type": string };
 7 |   private readonly workspaceId: string;
 8 | 
 9 |   constructor(apiToken: string, workspaceId: string) {
10 |     this.workspaceId = workspaceId;
11 |     this.headers = {
12 |       Authorization: apiToken,
13 |       "Content-Type": "application/json",
14 |     };
15 |   }
16 | 
17 |   private async request<T>(
18 |     endpoint: string,
19 |     options: RequestInit = {}
20 |   ): Promise<T> {
21 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
22 |       ...options,
23 |       headers: this.headers,
24 |     });
25 |     return response.json();
26 |   }
27 | 
28 |   async getSpaces() {
29 |     return this.request<{ spaces: any[] }>(`/team/${this.workspaceId}/space`);
30 |   }
31 | }
32 | 
33 | export default SpaceService;
34 | 
```

--------------------------------------------------------------------------------
/src/controllers/folder.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dottenv from "dotenv";
 2 | import { defineTool } from "../utils/defineTool";
 3 | import FolderService from "../services/folder.service";
 4 | 
 5 | dottenv.config();
 6 | const apiToken = process.env.CLICKUP_API_TOKEN;
 7 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 8 | 
 9 | if (!apiToken || !workspaceId) {
10 |   console.error(
11 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
12 |   );
13 |   process.exit(1);
14 | }
15 | 
16 | const folderService = new FolderService(apiToken, workspaceId);
17 | 
18 | const getFoldersTool = defineTool((z) => ({
19 |   name: "get_folders",
20 |   description: "Get all folders in a space",
21 |   inputSchema: {
22 |     space_id: z.string().describe("ClickUp space ID"),
23 |   },
24 |   handler: async (input) => {
25 |     const { space_id } = input;
26 |     const response = await folderService.getFolders(space_id);
27 |     return {
28 |       content: [{ type: "text", text: JSON.stringify(response) }],
29 |     };
30 |   },
31 | }));
32 | 
33 | export { getFoldersTool };
34 | 
```

--------------------------------------------------------------------------------
/src/services/assignee.service.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ClickUpUser } from "../models/types";
 2 | 
 3 | const BASE_URL = "https://api.clickup.com/api/v2";
 4 | 
 5 | export class AssigneeService {
 6 |   private readonly headers: { Authorization: string; "Content-Type": string };
 7 |   private readonly workspaceId: string;
 8 | 
 9 |   constructor(apiToken: string, workspaceId: string) {
10 |     this.workspaceId = workspaceId;
11 |     this.headers = {
12 |       Authorization: apiToken,
13 |       "Content-Type": "application/json",
14 |     };
15 |   }
16 | 
17 |   private async request<T>(
18 |     endpoint: string,
19 |     options: RequestInit = {}
20 |   ): Promise<T> {
21 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
22 |       ...options,
23 |       headers: this.headers,
24 |     });
25 |     return response.json();
26 |   }
27 | 
28 |   async getListMembers(listId: string) {
29 |     // Using the endpoint from https://developer.clickup.com/reference/getlistmembers
30 |     return this.request<{ members: ClickUpUser[] }>(`/list/${listId}/member`);
31 |   }
32 | }
33 | 
34 | export default AssigneeService;
35 | 
```

--------------------------------------------------------------------------------
/src/controllers/assignee.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dottenv from "dotenv";
 2 | import { defineTool } from "../utils/defineTool";
 3 | import AssigneeService from "../services/assignee.service";
 4 | 
 5 | dottenv.config();
 6 | const apiToken = process.env.CLICKUP_API_TOKEN;
 7 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 8 | 
 9 | if (!apiToken || !workspaceId) {
10 |   console.error(
11 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
12 |   );
13 |   process.exit(1);
14 | }
15 | 
16 | const assigneeService = new AssigneeService(apiToken, workspaceId);
17 | 
18 | const getListAssigneesTool = defineTool((z) => ({
19 |   name: "get_list_assignees",
20 |   description: "Get all members (potential assignees) of a list",
21 |   inputSchema: {
22 |     list_id: z.string().describe("ClickUp list ID"),
23 |   },
24 |   handler: async (input) => {
25 |     const { list_id } = input;
26 |     const response = await assigneeService.getListMembers(list_id);
27 |     return {
28 |       content: [{ type: "text", text: JSON.stringify(response) }],
29 |     };
30 |   },
31 | }));
32 | 
33 | export { getListAssigneesTool };
34 | 
```

--------------------------------------------------------------------------------
/src/services/list.service.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ClickUpUser } from "../models/types";
 2 | 
 3 | const BASE_URL = "https://api.clickup.com/api/v2";
 4 | 
 5 | export class ListService {
 6 |   private readonly headers: { Authorization: string; "Content-Type": string };
 7 |   private readonly workspaceId: string;
 8 | 
 9 |   constructor(apiToken: string, workspaceId: string) {
10 |     this.workspaceId = workspaceId;
11 |     this.headers = {
12 |       Authorization: apiToken,
13 |       "Content-Type": "application/json",
14 |     };
15 |   }
16 | 
17 |   private async request<T>(
18 |     endpoint: string,
19 |     options: RequestInit = {}
20 |   ): Promise<T> {
21 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
22 |       ...options,
23 |       headers: this.headers,
24 |     });
25 |     return response.json();
26 |   }
27 | 
28 |   async getLists(folderId: string) {
29 |     return this.request<{ lists: any[] }>(`/folder/${folderId}/list`);
30 |   }
31 | 
32 |   async createList(
33 |     folderId: string,
34 |     params: {
35 |       name: string;
36 |     }
37 |   ) {
38 |     return this.request(`/folder/${folderId}/list`, {
39 |       method: "POST",
40 |       body: JSON.stringify(params),
41 |     });
42 |   }
43 | }
44 | 
45 | export default ListService;
46 | 
```

--------------------------------------------------------------------------------
/.github/workflows/pr_agent.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: PR Agent
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     types: [opened, reopened, ready_for_review]
 6 |   issue_comment:
 7 | 
 8 | jobs:
 9 |   pr_agent_job:
10 |     if: ${{ github.event.sender.type != 'Bot' }}
11 |     runs-on: ubuntu-latest
12 |     permissions:
13 |       issues: write
14 |       pull-requests: write
15 |       contents: write
16 |     name: Run pr agent on every pull request, respond to user comments
17 |     steps:
18 |       - name: PR Agent action step
19 |         id: pragent
20 |         uses: docker://codiumai/pr-agent:0.30-github_action
21 |         env:
22 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 |           OPENAI_KEY: ${{ secrets.PR_AGENT_OPENAI_KEY }}
24 |           # Custom review instructions
25 |           pr_reviewer.extra_instructions: "Review security vulnerabilities and performance issues. Check for proper error handling. Check for spelling errors in user-facing text, API names, or documentation"
26 |           config.ai_timeout: "300"
27 |           # Tool configuration
28 |           github_action_config.auto_review: "true"
29 |           github_action_config.auto_describe: "true"
30 |           github_action_config.auto_improve: "true"
31 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "clickup-mcp-server",
 3 |   "description": "Model Context Protocol server for the ClickUp API",
 4 |   "version": "1.0.0",
 5 |   "author": "Leanware-io",
 6 |   "license": "MIT",
 7 |   "type": "module",
 8 |   "bin": "dist/index.js",
 9 |   "files": [
10 |     "dist"
11 |   ],
12 |   "keywords": [
13 |     "mcp",
14 |     "clickup",
15 |     "cline",
16 |     "claude",
17 |     "model context protocol"
18 |   ],
19 |   "scripts": {
20 |     "build": "tsup src/index.ts --format esm,cjs --dts",
21 |     "start": "node dist/index.js",
22 |     "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
23 |     "clean": "npx rimraf dist"
24 |   },
25 |   "repository": {
26 |     "type": "git",
27 |     "url": "https://github.com/Leanware-io/clickup-mcp-server.git"
28 |   },
29 |   "bugs": {
30 |     "url": "https://github.com/Leanware-io/clickup-mcp-server/issues"
31 |   },
32 |   "homepage": "https://github.com/Leanware-io/clickup-mcp-server#readme",
33 |   "dependencies": {
34 |     "@modelcontextprotocol/sdk": "^1.5.0",
35 |     "dotenv": "^16.4.7",
36 |     "tsup": "^8.3.6",
37 |     "zod": "^3.24.2"
38 |   },
39 |   "devDependencies": {
40 |     "@types/node": "^22.13.4",
41 |     "shx": "^0.3.4",
42 |     "typescript": "^5.6.2"
43 |   }
44 | }
45 | 
```

--------------------------------------------------------------------------------
/src/services/folder.service.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ClickUpUser } from "../models/types";
 2 | 
 3 | const BASE_URL = "https://api.clickup.com/api/v2";
 4 | 
 5 | export class FolderService {
 6 |   private readonly headers: { Authorization: string; "Content-Type": string };
 7 |   private readonly workspaceId: string;
 8 | 
 9 |   constructor(apiToken: string, workspaceId: string) {
10 |     this.workspaceId = workspaceId;
11 |     this.headers = {
12 |       Authorization: apiToken,
13 |       "Content-Type": "application/json",
14 |     };
15 |   }
16 | 
17 |   private async request<T>(
18 |     endpoint: string,
19 |     options: RequestInit = {}
20 |   ): Promise<T> {
21 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
22 |       ...options,
23 |       headers: this.headers,
24 |     });
25 |     return response.json();
26 |   }
27 | 
28 |   async getFolders(spaceId: string) {
29 |     const response = await this.request<{ folders: any[] }>(
30 |       `/space/${spaceId}/folder`
31 |     );
32 | 
33 |     // Remove the "lists" attribute from each folder to reduce payload size
34 |     if (response.folders && Array.isArray(response.folders)) {
35 |       response.folders = response.folders.map((folder) => {
36 |         const { lists, ...folderWithoutLists } = folder;
37 |         return folderWithoutLists;
38 |       });
39 |     }
40 | 
41 |     return response;
42 |   }
43 | }
44 | 
45 | export default FolderService;
46 | 
```

--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export class ToolError extends Error {
 2 |   constructor(
 3 |     message: string,
 4 |     public code: string,
 5 |     public details?: Record<string, any>
 6 |   ) {
 7 |     super(message);
 8 |     this.name = "ToolError";
 9 |   }
10 | }
11 | 
12 | export function formatErrorResponse(error: unknown) {
13 |   if (error instanceof ToolError) {
14 |     return {
15 |       content: [
16 |         {
17 |           type: "text",
18 |           text: JSON.stringify({
19 |             error: {
20 |               code: error.code,
21 |               message: error.message,
22 |               details: error.details,
23 |             },
24 |           }),
25 |         },
26 |       ],
27 |     };
28 |   }
29 | 
30 |   if (error instanceof Error) {
31 |     return {
32 |       content: [
33 |         {
34 |           type: "text",
35 |           text: JSON.stringify({
36 |             error: {
37 |               code: "INTERNAL_ERROR",
38 |               message: error.message,
39 |               stack:
40 |                 process.env.NODE_ENV === "development"
41 |                   ? error.stack
42 |                   : undefined,
43 |             },
44 |           }),
45 |         },
46 |       ],
47 |     };
48 |   }
49 | 
50 |   return {
51 |     content: [
52 |       {
53 |         type: "text",
54 |         text: JSON.stringify({
55 |           error: {
56 |             code: "UNKNOWN_ERROR",
57 |             message: String(error),
58 |           },
59 |         }),
60 |       },
61 |     ],
62 |   };
63 | }
64 | 
```

--------------------------------------------------------------------------------
/src/controllers/list.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dottenv from "dotenv";
 2 | import { defineTool } from "../utils/defineTool";
 3 | import ListService from "../services/list.service";
 4 | 
 5 | dottenv.config();
 6 | const apiToken = process.env.CLICKUP_API_TOKEN;
 7 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 8 | 
 9 | if (!apiToken || !workspaceId) {
10 |   console.error(
11 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
12 |   );
13 |   process.exit(1);
14 | }
15 | 
16 | const listService = new ListService(apiToken, workspaceId);
17 | 
18 | const getListsTool = defineTool((z) => ({
19 |   name: "get_lists",
20 |   description: "Get all lists in a folder",
21 |   inputSchema: {
22 |     folder_id: z.string().describe("ClickUp folder ID"),
23 |   },
24 |   handler: async (input) => {
25 |     const { folder_id } = input;
26 |     const response = await listService.getLists(folder_id);
27 |     return {
28 |       content: [{ type: "text", text: JSON.stringify(response) }],
29 |     };
30 |   },
31 | }));
32 | 
33 | const createListTool = defineTool((z) => ({
34 |   name: "create_list",
35 |   description: "Create a new list in a folder",
36 |   inputSchema: {
37 |     folder_id: z.string().describe("ClickUp folder ID"),
38 |     name: z.string().describe("List name"),
39 |   },
40 |   handler: async (input) => {
41 |     const { folder_id, name } = input;
42 |     const response = await listService.createList(folder_id, {
43 |       name,
44 |     });
45 |     return {
46 |       content: [{ type: "text", text: JSON.stringify(response) }],
47 |     };
48 |   },
49 | }));
50 | 
51 | export { getListsTool, createListTool };
52 | 
```

--------------------------------------------------------------------------------
/src/utils/defineTool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
 2 | import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
 3 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 4 | import { z } from "zod";
 5 | 
 6 | export type InferToolHandlerInput<TInputSchema extends z.ZodRawShape> =
 7 |   z.objectOutputType<TInputSchema, z.ZodTypeAny>;
 8 | 
 9 | type ToolDefinition<TInputSchema extends z.ZodRawShape> = {
10 |   name: string;
11 |   description: string;
12 |   inputSchema: TInputSchema;
13 |   handler: (
14 |     input: InferToolHandlerInput<TInputSchema>
15 |   ) => Promise<Record<string, unknown>>;
16 | };
17 | 
18 | export const defineTool = <TInputSchema extends z.ZodRawShape>(
19 |   cb: (zod: typeof z) => ToolDefinition<TInputSchema>
20 | ) => {
21 |   const tool = cb(z);
22 | 
23 |   const wrappedHandler = async (
24 |     input: InferToolHandlerInput<TInputSchema>,
25 |     _: RequestHandlerExtra
26 |   ): Promise<CallToolResult> => {
27 |     try {
28 |       const result = await tool.handler(input);
29 |       return {
30 |         content: [
31 |           {
32 |             type: "text",
33 |             text: JSON.stringify(result, null, 2),
34 |           },
35 |         ],
36 |       };
37 |     } catch (error) {
38 |       return {
39 |         content: [
40 |           {
41 |             type: "text",
42 |             text: `Error: ${
43 |               error instanceof Error ? error.message : String(error)
44 |             }`,
45 |           },
46 |         ],
47 |         isError: true,
48 |       };
49 |     }
50 |   };
51 | 
52 |   return {
53 |     ...tool,
54 |     handler: wrappedHandler as ToolCallback<TInputSchema>,
55 |   };
56 | };
57 | 
```

--------------------------------------------------------------------------------
/src/services/custom-field.service.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ClickUpCustomField } from "../models/types";
 2 | 
 3 | const BASE_URL = "https://api.clickup.com/api/v2";
 4 | 
 5 | export class CustomFieldService {
 6 |   private readonly headers: { Authorization: string; "Content-Type": string };
 7 |   private readonly workspaceId: string;
 8 | 
 9 |   constructor(apiToken: string, workspaceId: string) {
10 |     this.workspaceId = workspaceId;
11 |     this.headers = {
12 |       Authorization: apiToken,
13 |       "Content-Type": "application/json",
14 |     };
15 |   }
16 | 
17 |   private async request<T>(
18 |     endpoint: string,
19 |     options: RequestInit = {}
20 |   ): Promise<T> {
21 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
22 |       ...options,
23 |       headers: this.headers,
24 |     });
25 |     return response.json();
26 |   }
27 | 
28 |   async getListCustomFields(
29 |     listId: string
30 |   ): Promise<{ fields: ClickUpCustomField[] }> {
31 |     return this.request<{ fields: ClickUpCustomField[] }>(
32 |       `/list/${listId}/field`
33 |     );
34 |   }
35 | 
36 |   async setCustomFieldValue(
37 |     taskId: string,
38 |     customFieldId: string,
39 |     value: any
40 |   ): Promise<{ field: ClickUpCustomField }> {
41 |     return this.request<{ field: ClickUpCustomField }>(
42 |       `/task/${taskId}/field/${customFieldId}`,
43 |       {
44 |         method: "POST",
45 |         body: JSON.stringify({ value }),
46 |       }
47 |     );
48 |   }
49 | 
50 |   async setCustomFieldValueByCustomId(
51 |     customId: string,
52 |     customFieldId: string,
53 |     value: any
54 |   ): Promise<{ field: ClickUpCustomField }> {
55 |     return this.request<{ field: ClickUpCustomField }>(
56 |       `/task/${customId}/field/${customFieldId}?custom_task_ids=true&team_id=${this.workspaceId}`,
57 |       {
58 |         method: "POST",
59 |         body: JSON.stringify({ value }),
60 |       }
61 |     );
62 |   }
63 | }
64 | 
65 | export default CustomFieldService;
66 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4 | import {
  5 |   getTaskByCustomIdTool,
  6 |   getTaskTool,
  7 |   createTaskTool,
  8 |   updateTaskTool,
  9 |   updateTaskByCustomIdTool,
 10 | } from "./controllers/task.controller";
 11 | import {
 12 |   searchDocsTool,
 13 |   createDocTool,
 14 |   getDocPagesTool,
 15 |   getPageTool,
 16 |   createPageTool,
 17 |   editPageTool,
 18 | } from "./controllers/docs.controller";
 19 | import { getSpacesTool } from "./controllers/space.controller";
 20 | import { getFoldersTool } from "./controllers/folder.controller";
 21 | import { getListsTool, createListTool } from "./controllers/list.controller";
 22 | import {
 23 |   getListCustomFieldsTool,
 24 |   setCustomFieldValueTool,
 25 |   setCustomFieldValueByCustomIdTool,
 26 | } from "./controllers/custom-field.controller";
 27 | import { getListAssigneesTool } from "./controllers/assignee.controller";
 28 | 
 29 | const tools = [
 30 |   // Task tools
 31 |   getTaskByCustomIdTool,
 32 |   getTaskTool,
 33 |   createTaskTool,
 34 |   updateTaskTool,
 35 |   updateTaskByCustomIdTool,
 36 | 
 37 |   // Space tools
 38 |   getSpacesTool,
 39 | 
 40 |   // Folder tools
 41 |   getFoldersTool,
 42 | 
 43 |   // List tools
 44 |   getListsTool,
 45 |   createListTool,
 46 | 
 47 |   // Custom Field tools
 48 |   getListCustomFieldsTool,
 49 |   setCustomFieldValueTool,
 50 |   setCustomFieldValueByCustomIdTool,
 51 | 
 52 |   // Assignee tools
 53 |   getListAssigneesTool,
 54 | 
 55 |   // Docs tools
 56 |   searchDocsTool,
 57 |   createDocTool,
 58 |   getDocPagesTool,
 59 |   getPageTool,
 60 |   createPageTool,
 61 |   editPageTool,
 62 | ];
 63 | 
 64 | async function main() {
 65 |   console.error("Starting ClickUp MCP Server...");
 66 | 
 67 |   const apiToken = process.env.CLICKUP_API_TOKEN;
 68 |   const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 69 | 
 70 |   if (!apiToken || !workspaceId) {
 71 |     console.error(
 72 |       "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
 73 |     );
 74 |     process.exit(1);
 75 |   }
 76 | 
 77 |   const server = new McpServer(
 78 |     {
 79 |       name: "ClickUp MCP Server",
 80 |       version: "1.0.0",
 81 |     },
 82 |     {
 83 |       capabilities: {
 84 |         tools: {},
 85 |       },
 86 |     }
 87 |   );
 88 | 
 89 |   tools.forEach((tool) => {
 90 |     server.tool(tool.name, tool.description, tool.inputSchema, tool.handler);
 91 |   });
 92 | 
 93 |   const transport = new StdioServerTransport();
 94 |   console.error("Connecting server to transport...");
 95 |   await server.connect(transport);
 96 | 
 97 |   console.error("ClickUp MCP Server running on stdio");
 98 | }
 99 | 
100 | main().catch((error) => {
101 |   console.error("Fatal error in main():", error);
102 |   process.exit(1);
103 | });
104 | 
```

--------------------------------------------------------------------------------
/src/models/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import z from "zod";
 2 | import {
 3 |   ClickUpTaskSchema,
 4 |   ClickUpUserSchema,
 5 |   ClickUpCustomFieldSchema,
 6 |   ClickUpDocSchema,
 7 |   ClickUpDocPageSchema,
 8 | } from "./schema";
 9 | 
10 | export type ClickUpUser = z.infer<typeof ClickUpUserSchema>;
11 | export type ClickUpTask = z.infer<typeof ClickUpTaskSchema>;
12 | export type ClickUpCustomField = z.infer<typeof ClickUpCustomFieldSchema>;
13 | export type ClickUpDoc = z.infer<typeof ClickUpDocSchema>;
14 | export type ClickUpDocPage = z.infer<typeof ClickUpDocPageSchema>;
15 | 
16 | export interface GetListTasksParams {
17 |   archived?: boolean;
18 |   page?: number;
19 |   subtasks?: boolean;
20 |   include_closed?: boolean;
21 | }
22 | 
23 | export interface CreateTaskParams {
24 |   name: string;
25 |   description?: string;
26 |   markdown_description?: string; // Task description in markdown format
27 |   list_id: string;
28 |   priority?: number; // 1 (Urgent), 2 (High), 3 (Normal), 4 (Low)
29 |   due_date?: number; // Unix timestamp in milliseconds
30 |   tags?: string[]; // Array of tag names
31 |   time_estimate?: number; // Time estimate in milliseconds
32 |   assignees?: number[]; // Array of user IDs to assign to the task
33 |   custom_fields?: Array<{
34 |     id: string;
35 |     value: string | number | boolean | any[] | Record<string, any>;
36 |   }>; // Custom fields to set on task creation
37 |   parent?: string; // Parent task ID to create this task as a subtask
38 | }
39 | 
40 | export interface UpdateTaskParams {
41 |   name?: string;
42 |   description?: string;
43 |   markdown_description?: string; // Task description in markdown format
44 |   priority?: number; // 1 (Urgent), 2 (High), 3 (Normal), 4 (Low)
45 |   due_date?: number; // Unix timestamp in milliseconds
46 |   tags?: string[]; // Array of tag names
47 |   time_estimate?: number; // Time estimate in milliseconds
48 |   assignees?: {
49 |     add?: number[]; // Array of user IDs to add to the task
50 |     rem?: number[]; // Array of user IDs to remove from the task
51 |   };
52 |   parent?: string; // Parent task ID to move this task as a subtask
53 | }
54 | 
55 | export interface SearchDocsParams {
56 |   parent_type: string; // Type of parent (e.g., "workspace", "folder", "list")
57 |   parent_id: string; // ID of the parent
58 | }
59 | 
60 | export interface CreateDocParams {
61 |   name: string;
62 |   parent: {
63 |     id: string;
64 |     type: number; // 4 for Space, 5 for Folder, 6 for List, 7 for Everything, 12 for Workspace
65 |   };
66 |   visibility?: string; // "PRIVATE" by default
67 |   create_page?: boolean; // false by default
68 | }
69 | 
70 | export interface CreatePageParams {
71 |   docId: string;
72 |   name: string;
73 |   parent_page_id?: string;
74 |   sub_title?: string;
75 |   content: string;
76 | }
77 | 
78 | export interface EditPageParams {
79 |   docId: string;
80 |   pageId: string;
81 |   name?: string;
82 |   sub_title?: string;
83 |   content?: string;
84 |   content_edit_mode?: string;
85 | }
86 | 
```

--------------------------------------------------------------------------------
/src/services/task.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   ClickUpTask,
  3 |   ClickUpUser,
  4 |   CreateTaskParams,
  5 |   UpdateTaskParams,
  6 |   GetListTasksParams,
  7 | } from "../models/types";
  8 | 
  9 | const BASE_URL = "https://api.clickup.com/api/v2";
 10 | 
 11 | export class TaskService {
 12 |   private readonly headers: { Authorization: string; "Content-Type": string };
 13 |   private readonly workspaceId: string;
 14 | 
 15 |   constructor(apiToken: string, workspaceId: string) {
 16 |     this.workspaceId = workspaceId;
 17 |     this.headers = {
 18 |       Authorization: apiToken,
 19 |       "Content-Type": "application/json",
 20 |     };
 21 |   }
 22 | 
 23 |   private async request<T>(
 24 |     endpoint: string,
 25 |     options: RequestInit = {}
 26 |   ): Promise<T> {
 27 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
 28 |       ...options,
 29 |       headers: this.headers,
 30 |     });
 31 |     return response.json();
 32 |   }
 33 | 
 34 |   async getTask(taskId: string): Promise<ClickUpTask> {
 35 |     return this.request<ClickUpTask>(
 36 |       `/task/${taskId}?custom_task_ids=false&team_id=${this.workspaceId}&include_subtasks=true&include_markdown_description=true`
 37 |     );
 38 |   }
 39 | 
 40 |   async getTaskByCustomId(customId: string): Promise<ClickUpTask> {
 41 |     return this.request<ClickUpTask>(
 42 |       `/task/${customId}?custom_task_ids=true&team_id=${this.workspaceId}&include_subtasks=true&include_markdown_description=true`
 43 |     );
 44 |   }
 45 | 
 46 |   async createTask(params: CreateTaskParams): Promise<ClickUpTask> {
 47 |     const { list_id, ...taskData } = params;
 48 | 
 49 |     return this.request<ClickUpTask>(`/list/${list_id}/task`, {
 50 |       method: "POST",
 51 |       body: JSON.stringify(taskData),
 52 |     });
 53 |   }
 54 | 
 55 |   async updateTask(
 56 |     taskId: string,
 57 |     params: UpdateTaskParams
 58 |   ): Promise<ClickUpTask> {
 59 |     return this.request<ClickUpTask>(`/task/${taskId}`, {
 60 |       method: "PUT",
 61 |       body: JSON.stringify(params),
 62 |     });
 63 |   }
 64 | 
 65 |   async updateTaskByCustomId(
 66 |     customId: string,
 67 |     params: UpdateTaskParams
 68 |   ): Promise<ClickUpTask> {
 69 |     return this.request<ClickUpTask>(
 70 |       `/task/${customId}?custom_task_ids=true&team_id=${this.workspaceId}`,
 71 |       {
 72 |         method: "PUT",
 73 |         body: JSON.stringify(params),
 74 |       }
 75 |     );
 76 |   }
 77 | 
 78 |   async getListTasks(listId: string, params: GetListTasksParams = {}) {
 79 |     const queryParams = new URLSearchParams();
 80 | 
 81 |     // Add optional query parameters if they exist
 82 |     if (params.archived !== undefined)
 83 |       queryParams.append("archived", params.archived.toString());
 84 |     if (params.page !== undefined)
 85 |       queryParams.append("page", params.page.toString());
 86 |     if (params.subtasks !== undefined)
 87 |       queryParams.append("subtasks", params.subtasks.toString());
 88 |     if (params.include_closed !== undefined)
 89 |       queryParams.append("include_closed", params.include_closed.toString());
 90 | 
 91 |     const queryString = queryParams.toString()
 92 |       ? `?${queryParams.toString()}`
 93 |       : "";
 94 | 
 95 |     return this.request<{ tasks: ClickUpTask[] }>(
 96 |       `/list/${listId}/task${queryString}`
 97 |     );
 98 |   }
 99 | }
100 | 
101 | export default TaskService;
102 | 
```

--------------------------------------------------------------------------------
/src/controllers/custom-field.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import dottenv from "dotenv";
  2 | import { defineTool } from "../utils/defineTool";
  3 | import CustomFieldService from "../services/custom-field.service";
  4 | 
  5 | dottenv.config();
  6 | const apiToken = process.env.CLICKUP_API_TOKEN;
  7 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
  8 | 
  9 | if (!apiToken || !workspaceId) {
 10 |   console.error(
 11 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
 12 |   );
 13 |   process.exit(1);
 14 | }
 15 | 
 16 | const customFieldService = new CustomFieldService(apiToken, workspaceId);
 17 | 
 18 | const getListCustomFieldsTool = defineTool((z) => ({
 19 |   name: "clickup_get_list_custom_fields",
 20 |   description: "Get all accessible custom fields for a list",
 21 |   inputSchema: {
 22 |     list_id: z.string().describe("ClickUp list ID"),
 23 |   },
 24 |   handler: async (input) => {
 25 |     const { list_id } = input;
 26 |     const response = await customFieldService.getListCustomFields(list_id);
 27 |     return {
 28 |       content: [{ type: "text", text: JSON.stringify(response) }],
 29 |     };
 30 |   },
 31 | }));
 32 | 
 33 | const setCustomFieldValueTool = defineTool((z) => ({
 34 |   name: "clickup_set_custom_field_value",
 35 |   description: "Set a value for a custom field on a task",
 36 |   inputSchema: {
 37 |     task_id: z.string().describe("ClickUp task ID"),
 38 |     custom_field_id: z.string().describe("Custom field ID"),
 39 |     value: z
 40 |       .union([
 41 |         z.string(),
 42 |         z.number(),
 43 |         z.boolean(),
 44 |         z.array(z.unknown()),
 45 |         z.record(z.unknown()),
 46 |       ])
 47 |       .describe(
 48 |         "Value to set for the custom field. Type depends on the custom field type."
 49 |       ),
 50 |   },
 51 |   handler: async (input) => {
 52 |     const { task_id, custom_field_id, value } = input;
 53 |     const response = await customFieldService.setCustomFieldValue(
 54 |       task_id,
 55 |       custom_field_id,
 56 |       value
 57 |     );
 58 |     return {
 59 |       content: [{ type: "text", text: JSON.stringify(response) }],
 60 |     };
 61 |   },
 62 | }));
 63 | 
 64 | const setCustomFieldValueByCustomIdTool = defineTool((z) => ({
 65 |   name: "clickup_set_custom_field_value_by_custom_id",
 66 |   description:
 67 |     "Set a value for a custom field on a task using the task's custom ID",
 68 |   inputSchema: {
 69 |     custom_id: z.string().describe("ClickUp custom task ID"),
 70 |     custom_field_id: z.string().describe("Custom field ID"),
 71 |     value: z
 72 |       .union([
 73 |         z.string(),
 74 |         z.number(),
 75 |         z.boolean(),
 76 |         z.array(z.unknown()),
 77 |         z.record(z.unknown()),
 78 |       ])
 79 |       .describe(
 80 |         "Value to set for the custom field. Type depends on the custom field type."
 81 |       ),
 82 |   },
 83 |   handler: async (input) => {
 84 |     const { custom_id, custom_field_id, value } = input;
 85 |     const response = await customFieldService.setCustomFieldValueByCustomId(
 86 |       custom_id,
 87 |       custom_field_id,
 88 |       value
 89 |     );
 90 |     return {
 91 |       content: [{ type: "text", text: JSON.stringify(response) }],
 92 |     };
 93 |   },
 94 | }));
 95 | 
 96 | export {
 97 |   getListCustomFieldsTool,
 98 |   setCustomFieldValueTool,
 99 |   setCustomFieldValueByCustomIdTool,
100 | };
101 | 
```

--------------------------------------------------------------------------------
/src/services/docs.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   ClickUpDoc,
  3 |   ClickUpDocPage,
  4 |   CreateDocParams,
  5 |   CreatePageParams,
  6 |   EditPageParams,
  7 |   SearchDocsParams,
  8 | } from "../models/types";
  9 | 
 10 | const BASE_URL = "https://api.clickup.com/api/v3/workspaces";
 11 | 
 12 | export class DocsService {
 13 |   private readonly headers: { Authorization: string; "Content-Type": string };
 14 |   private readonly workspaceId: string;
 15 | 
 16 |   constructor(apiToken: string, workspaceId: string) {
 17 |     this.workspaceId = workspaceId;
 18 |     this.headers = {
 19 |       Authorization: apiToken,
 20 |       "Content-Type": "application/json",
 21 |     };
 22 |   }
 23 | 
 24 |   private async request<T>(
 25 |     endpoint: string,
 26 |     options: RequestInit = {}
 27 |   ): Promise<T> {
 28 |     const response = await fetch(`${BASE_URL}${endpoint}`, {
 29 |       ...options,
 30 |       headers: this.headers,
 31 |     });
 32 |     return response.json();
 33 |   }
 34 | 
 35 |   async searchDocs(params: SearchDocsParams): Promise<{ docs: ClickUpDoc[] }> {
 36 |     const { parent_type, parent_id } = params;
 37 |     return this.request<{ docs: ClickUpDoc[] }>(
 38 |       `/${this.workspaceId}/docs?parent_type=${parent_type}&parent_id=${parent_id}`
 39 |     );
 40 |   }
 41 | 
 42 |   async createDoc(params: CreateDocParams): Promise<ClickUpDoc> {
 43 |     const docData = {
 44 |       name: params.name,
 45 |       parent: params.parent,
 46 |       visibility: params.visibility || "PRIVATE",
 47 |       create_page:
 48 |         params.create_page !== undefined ? params.create_page : false,
 49 |     };
 50 | 
 51 |     return this.request<ClickUpDoc>(`/${this.workspaceId}/docs`, {
 52 |       method: "POST",
 53 |       body: JSON.stringify(docData),
 54 |     });
 55 |   }
 56 | 
 57 |   async getDocPages(docId: string): Promise<{ pages: ClickUpDocPage[] }> {
 58 |     return this.request<{ pages: ClickUpDocPage[] }>(
 59 |       `/${this.workspaceId}/docs/${docId}/pageListing`
 60 |     );
 61 |   }
 62 | 
 63 |   async getPage(docId: string, pageId: string): Promise<ClickUpDocPage> {
 64 |     return this.request<ClickUpDocPage>(
 65 |       `/${this.workspaceId}/docs/${docId}/pages/${pageId}`
 66 |     );
 67 |   }
 68 | 
 69 |   async createPage(params: CreatePageParams): Promise<ClickUpDocPage> {
 70 |     const { docId, name, parent_page_id, sub_title, content } = params;
 71 |     const pageData = {
 72 |       name,
 73 |       parent_page_id: parent_page_id || null,
 74 |       sub_title: sub_title || null,
 75 |       content,
 76 |       content_format: "text/md",
 77 |     };
 78 | 
 79 |     return this.request<ClickUpDocPage>(
 80 |       `/${this.workspaceId}/docs/${docId}/pages`,
 81 |       {
 82 |         method: "POST",
 83 |         body: JSON.stringify(pageData),
 84 |       }
 85 |     );
 86 |   }
 87 | 
 88 |   async editPage(params: EditPageParams): Promise<ClickUpDocPage> {
 89 |     const { docId, pageId, name, sub_title, content, content_edit_mode } =
 90 |       params;
 91 |     const pageData = {
 92 |       name,
 93 |       sub_title,
 94 |       content,
 95 |       content_edit_mode: content_edit_mode || "replace",
 96 |       content_format: "text/md",
 97 |     };
 98 | 
 99 |     return this.request<ClickUpDocPage>(
100 |       `/${this.workspaceId}/docs/${docId}/pages/${pageId}`,
101 |       {
102 |         method: "PUT",
103 |         body: JSON.stringify(pageData),
104 |       }
105 |     );
106 |   }
107 | }
108 | 
109 | export default DocsService;
110 | 
```

--------------------------------------------------------------------------------
/src/models/schema.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | 
  3 | export const ClickUpDocSchema = z.object({
  4 |   id: z.string(),
  5 |   title: z.string(),
  6 |   date_created: z.string(),
  7 |   date_updated: z.string(),
  8 |   folder: z
  9 |     .object({
 10 |       id: z.string(),
 11 |       name: z.string(),
 12 |       hidden: z.boolean(),
 13 |       access: z.boolean(),
 14 |     })
 15 |     .optional(),
 16 |   list: z
 17 |     .object({
 18 |       id: z.string(),
 19 |       name: z.string(),
 20 |       access: z.boolean(),
 21 |     })
 22 |     .optional(),
 23 |   project: z
 24 |     .object({
 25 |       id: z.string(),
 26 |       name: z.string(),
 27 |       hidden: z.boolean(),
 28 |       access: z.boolean(),
 29 |     })
 30 |     .optional(),
 31 |   space: z.object({
 32 |     id: z.string(),
 33 |   }),
 34 |   user: z.object({
 35 |     id: z.number(),
 36 |     username: z.string(),
 37 |     email: z.string(),
 38 |     color: z.string(),
 39 |   }),
 40 |   shared: z.boolean(),
 41 |   members: z.array(
 42 |     z.object({
 43 |       user: z.object({
 44 |         id: z.number(),
 45 |         username: z.string(),
 46 |         email: z.string(),
 47 |         color: z.string(),
 48 |       }),
 49 |       permission_level: z.string(),
 50 |     })
 51 |   ),
 52 |   permission_level: z.string(),
 53 |   parent: z.object({
 54 |     id: z.string(),
 55 |     type: z.number(),
 56 |   }),
 57 | });
 58 | 
 59 | export const ClickUpDocPageSchema = z.object({
 60 |   id: z.string(),
 61 |   title: z.string(),
 62 |   date_created: z.string(),
 63 |   date_updated: z.string(),
 64 |   parent: z.string().nullable(),
 65 |   content: z.string().optional(),
 66 |   children: z.array(z.string()).optional(),
 67 | });
 68 | 
 69 | export const ClickUpCustomFieldSchema = z.object({
 70 |   id: z.string(),
 71 |   name: z.string(),
 72 |   type: z.string(),
 73 |   type_config: z.unknown(),
 74 |   date_created: z.string(),
 75 |   hide_from_guests: z.boolean(),
 76 |   required: z.boolean(),
 77 |   value: z
 78 |     .union([
 79 |       z.string(),
 80 |       z.number(),
 81 |       z.null(),
 82 |       z.array(z.unknown()),
 83 |       z.record(z.unknown()),
 84 |     ])
 85 |     .optional(),
 86 | });
 87 | 
 88 | export const ClickUpUserSchema = z.object({
 89 |   id: z.number(),
 90 |   username: z.string(),
 91 |   email: z.string(),
 92 |   color: z.string(),
 93 |   profilePicture: z.string().url(),
 94 |   initials: z.string(),
 95 |   week_start_day: z.number(),
 96 |   global_font_support: z.string().nullable(),
 97 |   timezone: z.string(),
 98 | });
 99 | 
100 | export const ClickUpTaskSchema = z.object({
101 |   id: z.string(),
102 |   custom_id: z.string().nullable(),
103 |   custom_item_id: z.number().nullable(),
104 |   name: z.string(),
105 |   text_content: z.string(),
106 |   description: z.string(),
107 |   status: z.object({
108 |     status: z.string(),
109 |     color: z.string(),
110 |     orderindex: z.number(),
111 |     type: z.string(),
112 |   }),
113 |   orderindex: z.string(),
114 |   date_created: z.string(),
115 |   date_updated: z.string(),
116 |   date_closed: z.string().nullable(),
117 |   markdown_description: z.string().nullable(),
118 |   date_done: z.string().nullable(),
119 |   archived: z.boolean(),
120 |   creator: z.object({
121 |     id: z.number(),
122 |     username: z.string(),
123 |     color: z.string(),
124 |     email: z.string(),
125 |     profilePicture: z.string().nullable(),
126 |   }),
127 |   assignees: z.array(
128 |     z.object({
129 |       id: z.number(),
130 |       username: z.string(),
131 |       color: z.string().nullable(),
132 |       initials: z.string(),
133 |       email: z.string(),
134 |       profilePicture: z.string().nullable(),
135 |     })
136 |   ),
137 |   watchers: z.array(
138 |     z.object({
139 |       id: z.number(),
140 |       username: z.string(),
141 |       color: z.string(),
142 |       initials: z.string(),
143 |       email: z.string(),
144 |       profilePicture: z.string().nullable(),
145 |     })
146 |   ),
147 |   checklists: z.array(z.unknown()),
148 |   tags: z.array(
149 |     z.object({
150 |       name: z.string(),
151 |       tag_fg: z.string(),
152 |       tag_bg: z.string(),
153 |       creator: z.number(),
154 |     })
155 |   ),
156 |   parent: z.string().nullable(),
157 |   top_level_parent: z.string().nullable(),
158 |   priority: z.object({
159 |     color: z.string(),
160 |     id: z.string(),
161 |     orderindex: z.string(),
162 |     priority: z.string(),
163 |   }),
164 |   due_date: z.string().nullable(),
165 |   start_date: z.string().nullable(),
166 |   points: z.number().nullable(),
167 |   time_estimate: z.number().nullable(),
168 |   time_spent: z.number(),
169 |   custom_fields: z.array(
170 |     z.object({
171 |       id: z.string(),
172 |       name: z.string(),
173 |       type: z.string(),
174 |       type_config: z.unknown(),
175 |       date_created: z.string(),
176 |       hide_from_guests: z.boolean(),
177 |       required: z.boolean(),
178 |       value: z.union([z.string(), z.number(), z.null()]).optional(),
179 |     })
180 |   ),
181 |   dependencies: z.array(z.unknown()),
182 |   linked_tasks: z.array(z.unknown()),
183 |   locations: z.array(z.unknown()),
184 |   team_id: z.string(),
185 |   url: z.string(),
186 |   sharing: z.object({
187 |     public: z.boolean(),
188 |     public_share_expires_on: z.string().nullable(),
189 |     public_fields: z.array(z.string()),
190 |     token: z.string().nullable(),
191 |     seo_optimized: z.boolean(),
192 |   }),
193 |   permission_level: z.string(),
194 |   list: z.object({
195 |     id: z.string(),
196 |     name: z.string(),
197 |     access: z.boolean(),
198 |   }),
199 |   project: z.object({
200 |     id: z.string(),
201 |     name: z.string(),
202 |     hidden: z.boolean(),
203 |     access: z.boolean(),
204 |   }),
205 |   folder: z.object({
206 |     id: z.string(),
207 |     name: z.string(),
208 |     hidden: z.boolean(),
209 |     access: z.boolean(),
210 |   }),
211 |   space: z.object({
212 |     id: z.string(),
213 |   }),
214 |   attachments: z.array(z.unknown()),
215 | });
216 | 
```

--------------------------------------------------------------------------------
/src/controllers/docs.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import dottenv from "dotenv";
  2 | import { defineTool } from "../utils/defineTool";
  3 | import DocsService from "../services/docs.service";
  4 | import {
  5 |   SearchDocsParams,
  6 |   CreateDocParams,
  7 |   CreatePageParams,
  8 |   EditPageParams,
  9 | } from "../models/types";
 10 | 
 11 | dottenv.config();
 12 | const apiToken = process.env.CLICKUP_API_TOKEN;
 13 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 14 | 
 15 | if (!apiToken || !workspaceId) {
 16 |   console.error(
 17 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
 18 |   );
 19 |   process.exit(1);
 20 | }
 21 | 
 22 | const docsService = new DocsService(apiToken, workspaceId);
 23 | 
 24 | const searchDocsTool = defineTool((z) => ({
 25 |   name: "clickup_search_docs",
 26 |   description: "Search for docs in a specific parent",
 27 |   inputSchema: {
 28 |     parent_type: z
 29 |       .string()
 30 |       .describe("Type of parent (SPACE, FOLDER, LIST, EVERYTHING, WORKSPACE)"),
 31 |     parent_id: z.string().describe("ID of the parent"),
 32 |   },
 33 |   handler: async (input) => {
 34 |     const params: SearchDocsParams = {
 35 |       parent_type: input.parent_type,
 36 |       parent_id: input.parent_id,
 37 |     };
 38 |     const response = await docsService.searchDocs(params);
 39 |     return {
 40 |       content: [{ type: "text", text: JSON.stringify(response) }],
 41 |     };
 42 |   },
 43 | }));
 44 | 
 45 | const createDocTool = defineTool((z) => ({
 46 |   name: "clickup_create_doc",
 47 |   description: "Create a new doc in ClickUp",
 48 |   inputSchema: {
 49 |     name: z.string().describe("The name of the new Doc"),
 50 |     parent: z
 51 |       .object({
 52 |         id: z.string().describe("Parent ID"),
 53 |         type: z
 54 |           .number()
 55 |           .describe(
 56 |             "Parent type: 4 for Space, 5 for Folder, 6 for List, 7 for Everything, 12 for Workspace"
 57 |           ),
 58 |       })
 59 |       .describe("Parent object"),
 60 |     visibility: z
 61 |       .string()
 62 |       .optional()
 63 |       .describe("Doc visibility (PUBLIC or PRIVATE), PRIVATE by default"),
 64 |     create_page: z
 65 |       .boolean()
 66 |       .optional()
 67 |       .describe("Whether to create a initial page (false by default)"),
 68 |   },
 69 |   handler: async (input) => {
 70 |     const docParams: CreateDocParams = {
 71 |       name: input.name,
 72 |       parent: input.parent,
 73 |       visibility: input.visibility,
 74 |       create_page: input.create_page,
 75 |     };
 76 |     const response = await docsService.createDoc(docParams);
 77 |     return {
 78 |       content: [{ type: "text", text: JSON.stringify(response) }],
 79 |     };
 80 |   },
 81 | }));
 82 | 
 83 | const getDocPagesTool = defineTool((z) => ({
 84 |   name: "clickup_get_doc_pages",
 85 |   description: "Get pages from a ClickUp doc",
 86 |   inputSchema: {
 87 |     doc_id: z.string().describe("ClickUp doc ID"),
 88 |   },
 89 |   handler: async (input) => {
 90 |     const response = await docsService.getDocPages(input.doc_id);
 91 |     return {
 92 |       content: [{ type: "text", text: JSON.stringify(response) }],
 93 |     };
 94 |   },
 95 | }));
 96 | 
 97 | const getPageTool = defineTool((z) => ({
 98 |   name: "clickup_get_page",
 99 |   description: "Get a page from a ClickUp doc",
100 |   inputSchema: {
101 |     doc_id: z.string().describe("ClickUp doc ID"),
102 |     page_id: z.string().describe("ClickUp page ID"),
103 |   },
104 |   handler: async (input) => {
105 |     const response = await docsService.getPage(input.doc_id, input.page_id);
106 |     return {
107 |       content: [{ type: "text", text: JSON.stringify(response) }],
108 |     };
109 |   },
110 | }));
111 | 
112 | const createPageTool = defineTool((z) => ({
113 |   name: "clickup_create_page",
114 |   description: "Create a new page in a ClickUp doc",
115 |   inputSchema: {
116 |     doc_id: z.string().describe("ClickUp doc ID"),
117 |     name: z.string().describe("Page name"),
118 |     parent_page_id: z
119 |       .string()
120 |       .optional()
121 |       .describe("Parent page ID (null for root page)"),
122 |     sub_title: z.string().optional().describe("Page subtitle"),
123 |     content: z.string().describe("Page content in markdown format"),
124 |   },
125 |   handler: async (input) => {
126 |     const pageParams: CreatePageParams = {
127 |       docId: input.doc_id,
128 |       name: input.name,
129 |       parent_page_id: input.parent_page_id,
130 |       sub_title: input.sub_title,
131 |       content: input.content,
132 |     };
133 |     const response = await docsService.createPage(pageParams);
134 |     return {
135 |       content: [{ type: "text", text: JSON.stringify(response) }],
136 |     };
137 |   },
138 | }));
139 | 
140 | const editPageTool = defineTool((z) => ({
141 |   name: "clickup_edit_page",
142 |   description: "Edit a page in a ClickUp doc",
143 |   inputSchema: {
144 |     doc_id: z.string().describe("ClickUp doc ID"),
145 |     page_id: z.string().describe("ClickUp page ID"),
146 |     name: z.string().optional().describe("Page name"),
147 |     sub_title: z.string().optional().describe("Page subtitle"),
148 |     content: z.string().optional().describe("Page content in markdown format"),
149 |     content_edit_mode: z
150 |       .string()
151 |       .optional()
152 |       .describe(
153 |         "Content edit mode (replace, append, prepend), default is replace"
154 |       ),
155 |   },
156 |   handler: async (input) => {
157 |     const pageParams: EditPageParams = {
158 |       docId: input.doc_id,
159 |       pageId: input.page_id,
160 |       name: input.name,
161 |       sub_title: input.sub_title,
162 |       content: input.content,
163 |       content_edit_mode: input.content_edit_mode,
164 |     };
165 |     const response = await docsService.editPage(pageParams);
166 |     return {
167 |       content: [{ type: "text", text: JSON.stringify(response) }],
168 |     };
169 |   },
170 | }));
171 | 
172 | export {
173 |   searchDocsTool,
174 |   createDocTool,
175 |   getDocPagesTool,
176 |   getPageTool,
177 |   createPageTool,
178 |   editPageTool,
179 | };
180 | 
```

--------------------------------------------------------------------------------
/src/controllers/task.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import dottenv from "dotenv";
  2 | import { defineTool } from "../utils/defineTool";
  3 | import TaskService from "../services/task.service";
  4 | import {
  5 |   CreateTaskParams,
  6 |   UpdateTaskParams,
  7 |   GetListTasksParams,
  8 | } from "../models/types";
  9 | 
 10 | dottenv.config();
 11 | const apiToken = process.env.CLICKUP_API_TOKEN;
 12 | const workspaceId = process.env.CLICKUP_WORKSPACE_ID;
 13 | 
 14 | if (!apiToken || !workspaceId) {
 15 |   console.error(
 16 |     "Please set CLICKUP_API_TOKEN and CLICKUP_WORKSPACE_ID environment variables"
 17 |   );
 18 |   process.exit(1);
 19 | }
 20 | 
 21 | const taskService = new TaskService(apiToken, workspaceId);
 22 | 
 23 | const getTaskTool = defineTool((z) => ({
 24 |   name: "clickup_get_task",
 25 |   description: "Get a task by its ID",
 26 |   inputSchema: {
 27 |     task_id: z.string(),
 28 |   },
 29 |   handler: async (input) => {
 30 |     const { task_id } = input;
 31 |     const response = await taskService.getTask(task_id);
 32 |     return {
 33 |       content: [{ type: "text", text: JSON.stringify(response) }],
 34 |     };
 35 |   },
 36 | }));
 37 | 
 38 | const getTaskByCustomIdTool = defineTool((z) => ({
 39 |   name: "clickup_get_task_by_custom_id",
 40 |   description: "Get a task by its custom ID",
 41 |   inputSchema: {
 42 |     custom_id: z.string(),
 43 |   },
 44 |   handler: async (input) => {
 45 |     const { custom_id } = input;
 46 |     const response = await taskService.getTaskByCustomId(custom_id);
 47 |     return {
 48 |       content: [{ type: "text", text: JSON.stringify(response) }],
 49 |     };
 50 |   },
 51 | }));
 52 | 
 53 | const createTaskTool = defineTool((z) => ({
 54 |   name: "clickup_create_task",
 55 |   description: "Create a new task in ClickUp",
 56 |   inputSchema: {
 57 |     name: z.string().describe("Task name"),
 58 |     markdown_description: z
 59 |       .string()
 60 |       .optional()
 61 |       .describe("Task description in markdown format"),
 62 |     list_id: z.string().describe("ClickUp list ID"),
 63 |     priority: z
 64 |       .number()
 65 |       .optional()
 66 |       .describe("Task priority (1-4): 1=Urgent, 2=High, 3=Normal, 4=Low"),
 67 |     due_date: z
 68 |       .number()
 69 |       .optional()
 70 |       .describe("Due date as Unix timestamp in milliseconds"),
 71 |     tags: z
 72 |       .array(z.string())
 73 |       .optional()
 74 |       .describe("Array of tag names to add to the task"),
 75 |     time_estimate: z
 76 |       .number()
 77 |       .optional()
 78 |       .describe("Time estimate in milliseconds"),
 79 |     assignees: z
 80 |       .array(z.number())
 81 |       .optional()
 82 |       .describe("Array of user IDs to assign to the task"),
 83 |     custom_fields: z
 84 |       .array(
 85 |         z.object({
 86 |           id: z.string().describe("Custom field ID"),
 87 |           value: z
 88 |             .union([
 89 |               z.string(),
 90 |               z.number(),
 91 |               z.boolean(),
 92 |               z.array(z.unknown()),
 93 |               z.record(z.unknown()),
 94 |             ])
 95 |             .describe("Value for the custom field"),
 96 |         })
 97 |       )
 98 |       .optional()
 99 |       .describe("Custom fields to set on task creation"),
100 |     parent: z
101 |       .string()
102 |       .optional()
103 |       .describe("Parent task ID to create this task as a subtask"),
104 |   },
105 |   handler: async (input): Promise<any> => {
106 |     const taskParams: CreateTaskParams = {
107 |       name: input.name,
108 |       list_id: input.list_id,
109 |       markdown_description: input.markdown_description,
110 |       priority: input.priority,
111 |       due_date: input.due_date,
112 |       tags: input.tags,
113 |       time_estimate: input.time_estimate,
114 |       assignees: input.assignees,
115 |       custom_fields: input.custom_fields,
116 |       parent: input.parent,
117 |     };
118 | 
119 |     const response = await taskService.createTask(taskParams);
120 |     return {
121 |       content: [{ type: "text", text: JSON.stringify(response) }],
122 |     };
123 |   },
124 | }));
125 | 
126 | const updateTaskTool = defineTool((z) => ({
127 |   name: "clickup_update_task",
128 |   description: "Update a task by its ID",
129 |   inputSchema: {
130 |     task_id: z.string().describe("ClickUp task ID"),
131 |     name: z.string().optional().describe("Task name"),
132 |     markdown_description: z
133 |       .string()
134 |       .optional()
135 |       .describe("Task description in markdown format"),
136 |     priority: z
137 |       .number()
138 |       .optional()
139 |       .describe("Task priority (1-4): 1=Urgent, 2=High, 3=Normal, 4=Low"),
140 |     due_date: z
141 |       .number()
142 |       .optional()
143 |       .describe("Due date as Unix timestamp in milliseconds"),
144 |     tags: z
145 |       .array(z.string())
146 |       .optional()
147 |       .describe("Array of tag names to add to the task"),
148 |     time_estimate: z
149 |       .number()
150 |       .optional()
151 |       .describe("Time estimate in milliseconds"),
152 |     assignees: z
153 |       .object({
154 |         add: z
155 |           .array(z.number())
156 |           .optional()
157 |           .describe("Array of user IDs to add to the task"),
158 |         rem: z
159 |           .array(z.number())
160 |           .optional()
161 |           .describe("Array of user IDs to remove from the task"),
162 |       })
163 |       .optional()
164 |       .describe("User IDs to add or remove from the task"),
165 |     parent: z
166 |       .string()
167 |       .optional()
168 |       .describe("Parent task ID to move this task as a subtask"),
169 |   },
170 |   handler: async (input): Promise<any> => {
171 |     const { task_id, ...updateData } = input;
172 |     const taskParams: UpdateTaskParams = {
173 |       name: updateData.name,
174 |       markdown_description: updateData.markdown_description,
175 |       priority: updateData.priority,
176 |       due_date: updateData.due_date,
177 |       tags: updateData.tags,
178 |       time_estimate: updateData.time_estimate,
179 |       assignees: updateData.assignees,
180 |       parent: updateData.parent,
181 |     };
182 | 
183 |     const response = await taskService.updateTask(task_id, taskParams);
184 |     return {
185 |       content: [{ type: "text", text: JSON.stringify(response) }],
186 |     };
187 |   },
188 | }));
189 | 
190 | const updateTaskByCustomIdTool = defineTool((z) => ({
191 |   name: "clickup_update_task_by_custom_id",
192 |   description: "Update a task by its custom ID",
193 |   inputSchema: {
194 |     custom_id: z.string().describe("ClickUp custom task ID"),
195 |     name: z.string().optional().describe("Task name"),
196 |     markdown_description: z
197 |       .string()
198 |       .optional()
199 |       .describe("Task description in markdown format"),
200 |     priority: z
201 |       .number()
202 |       .optional()
203 |       .describe("Task priority (1-4): 1=Urgent, 2=High, 3=Normal, 4=Low"),
204 |     due_date: z
205 |       .number()
206 |       .optional()
207 |       .describe("Due date as Unix timestamp in milliseconds"),
208 |     tags: z
209 |       .array(z.string())
210 |       .optional()
211 |       .describe("Array of tag names to add to the task"),
212 |     time_estimate: z
213 |       .number()
214 |       .optional()
215 |       .describe("Time estimate in milliseconds"),
216 |     assignees: z
217 |       .object({
218 |         add: z
219 |           .array(z.number())
220 |           .optional()
221 |           .describe("Array of user IDs to add to the task"),
222 |         rem: z
223 |           .array(z.number())
224 |           .optional()
225 |           .describe("Array of user IDs to remove from the task"),
226 |       })
227 |       .optional()
228 |       .describe("User IDs to add or remove from the task"),
229 |     parent: z
230 |       .string()
231 |       .optional()
232 |       .describe("Parent task ID to move this task as a subtask"),
233 |   },
234 |   handler: async (input): Promise<any> => {
235 |     const { custom_id, ...updateData } = input;
236 |     const taskParams: UpdateTaskParams = {
237 |       name: updateData.name,
238 |       markdown_description: updateData.markdown_description,
239 |       priority: updateData.priority,
240 |       due_date: updateData.due_date,
241 |       tags: updateData.tags,
242 |       time_estimate: updateData.time_estimate,
243 |       assignees: updateData.assignees,
244 |       parent: updateData.parent,
245 |     };
246 | 
247 |     const response = await taskService.updateTaskByCustomId(
248 |       custom_id,
249 |       taskParams
250 |     );
251 |     return {
252 |       content: [{ type: "text", text: JSON.stringify(response) }],
253 |     };
254 |   },
255 | }));
256 | 
257 | const getListTasksTool = defineTool((z) => ({
258 |   name: "get_list_tasks",
259 |   description: "Get tasks from a ClickUp list with optional filtering",
260 |   inputSchema: {
261 |     list_id: z.string().describe("ClickUp list ID"),
262 |     archived: z.boolean().optional().describe("Include archived tasks"),
263 |     page: z.number().optional().describe("Page number for pagination"),
264 |     subtasks: z.boolean().optional().describe("Include subtasks"),
265 |     include_closed: z.boolean().optional().describe("Include closed tasks"),
266 |   },
267 |   handler: async (input) => {
268 |     const { list_id, ...params } = input;
269 |     const response = await taskService.getListTasks(
270 |       list_id,
271 |       params as GetListTasksParams
272 |     );
273 |     return {
274 |       content: [{ type: "text", text: JSON.stringify(response) }],
275 |     };
276 |   },
277 | }));
278 | 
279 | export {
280 |   getTaskByCustomIdTool,
281 |   getTaskTool,
282 |   createTaskTool,
283 |   updateTaskTool,
284 |   updateTaskByCustomIdTool,
285 |   getListTasksTool,
286 | };
287 | 
```