# 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 |
```