@d4works/mcp-clickup 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/dist/clickup.d.ts +148 -0
- package/dist/clickup.js +145 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +406 -0
- package/dist/test-connection.d.ts +1 -0
- package/dist/test-connection.js +17 -0
- package/dist/test-get-task.d.ts +1 -0
- package/dist/test-get-task.js +93 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.js +66 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @d4works/mcp-clickup
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for ClickUp task management. Allows Claude to read task details from ClickUp and help with implementation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Fetch task details (description, comments, attachments, custom fields, etc.)
|
|
8
|
+
- Support for multiple task ID formats (URL, custom ID, number)
|
|
9
|
+
- Instructions for Claude to analyze tasks before implementation
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
### 1. Environment Variables
|
|
14
|
+
|
|
15
|
+
| Variable | Required | Description |
|
|
16
|
+
| --------------------- | -------- | ------------------------------------------------ |
|
|
17
|
+
| `CLICKUP_API_TOKEN` | Yes | Your ClickUp API token |
|
|
18
|
+
| `CLICKUP_TEAM_ID` | Yes | Your ClickUp team/workspace ID |
|
|
19
|
+
| `CLICKUP_TASK_PREFIX` | No | Task prefix for number-only input (e.g., "PROJ") |
|
|
20
|
+
|
|
21
|
+
### 2. Get Your API Token
|
|
22
|
+
|
|
23
|
+
1. Open ClickUp → Click your avatar → **Settings**
|
|
24
|
+
2. Go to **Apps** section
|
|
25
|
+
3. Generate or copy your **API Token**
|
|
26
|
+
|
|
27
|
+
### 3. Find Your Team ID
|
|
28
|
+
|
|
29
|
+
1. Open ClickUp → Go to any workspace
|
|
30
|
+
2. Look at the URL: `https://app.clickup.com/{TEAM_ID}/...`
|
|
31
|
+
3. The number in the URL is your Team ID
|
|
32
|
+
|
|
33
|
+
### 4. Create MCP Configuration
|
|
34
|
+
|
|
35
|
+
Create `.mcp.json` in your project root.
|
|
36
|
+
|
|
37
|
+
First, store your API token in a local file that's gitignored:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
echo "export CLICKUP_API_TOKEN=pk_your_token_here" >> .claude/.env.local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then configure `.mcp.json` to source this file before running the server:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"clickup": {
|
|
49
|
+
"command": "bash",
|
|
50
|
+
"args": [
|
|
51
|
+
"-c",
|
|
52
|
+
"source .claude/.env.local && npx -y @d4works/mcp-clickup"
|
|
53
|
+
],
|
|
54
|
+
"env": {
|
|
55
|
+
"CLICKUP_TEAM_ID": "your_team_id",
|
|
56
|
+
"CLICKUP_TASK_PREFIX": "PROJ"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **Note:** If you have `CLICKUP_API_TOKEN` exported in your global `~/.zshrc` or `~/.bashrc`, you can simplify the command to just `npx -y @d4works/mcp-clickup`.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Ask Claude to work on a task using any of these formats:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
> Work on task ABC-123
|
|
71
|
+
> Work on task 123
|
|
72
|
+
> Work on task https://app.clickup.com/t/12345/ABC-123
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Claude will:
|
|
76
|
+
|
|
77
|
+
1. Fetch task details from ClickUp
|
|
78
|
+
2. Analyze the task (description, comments, attachments)
|
|
79
|
+
3. Propose an implementation plan
|
|
80
|
+
4. Wait for your approval before writing code
|
|
81
|
+
|
|
82
|
+
## Available Tools
|
|
83
|
+
|
|
84
|
+
### `test-connection`
|
|
85
|
+
|
|
86
|
+
Test connection to ClickUp API.
|
|
87
|
+
|
|
88
|
+
### `get-task`
|
|
89
|
+
|
|
90
|
+
Get task details from ClickUp (description, status, comments, attachments, custom fields, checklists, etc.).
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
|
|
94
|
+
- `taskId` (string, required): Task URL, custom ID (ABC-123), or number
|
|
95
|
+
|
|
96
|
+
### `get-task-statuses`
|
|
97
|
+
|
|
98
|
+
Get available statuses for a task. Use this before updating status to see valid options.
|
|
99
|
+
|
|
100
|
+
**Parameters:**
|
|
101
|
+
|
|
102
|
+
- `taskId` (string, required): Task URL, custom ID (ABC-123), or number
|
|
103
|
+
|
|
104
|
+
### `set-task-status`
|
|
105
|
+
|
|
106
|
+
Set the status of a ClickUp task. Only used with explicit user confirmation.
|
|
107
|
+
|
|
108
|
+
**Parameters:**
|
|
109
|
+
|
|
110
|
+
- `taskId` (string, required): Task URL, custom ID (ABC-123), or number
|
|
111
|
+
- `status` (string, required): New status name (must match one of the available statuses)
|
|
112
|
+
|
|
113
|
+
### `get-release-options`
|
|
114
|
+
|
|
115
|
+
Get available Release dropdown options for a task. Use this to see valid values before setting Release.
|
|
116
|
+
|
|
117
|
+
**Parameters:**
|
|
118
|
+
|
|
119
|
+
- `taskId` (string, required): Task URL, custom ID (ABC-123), or number
|
|
120
|
+
- `fieldName` (string, optional): Name of the dropdown field (default: "Release")
|
|
121
|
+
|
|
122
|
+
### `set-task-release`
|
|
123
|
+
|
|
124
|
+
Set the Release dropdown value on a ClickUp task. Only used with explicit user confirmation.
|
|
125
|
+
|
|
126
|
+
**Parameters:**
|
|
127
|
+
|
|
128
|
+
- `taskId` (string, required): Task URL, custom ID (ABC-123), or number
|
|
129
|
+
- `release` (string, required): Release value to set (must match one of the available options)
|
|
130
|
+
- `fieldName` (string, optional): Name of the dropdown field (default: "Release")
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp API client
|
|
3
|
+
*/
|
|
4
|
+
export interface ClickUpStatus {
|
|
5
|
+
id?: string;
|
|
6
|
+
status: string;
|
|
7
|
+
color: string;
|
|
8
|
+
orderindex: number;
|
|
9
|
+
type: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ClickUpList {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
statuses: ClickUpStatus[];
|
|
15
|
+
}
|
|
16
|
+
export interface ClickUpTask {
|
|
17
|
+
id: string;
|
|
18
|
+
custom_id: string | null;
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
status: {
|
|
22
|
+
status: string;
|
|
23
|
+
color: string;
|
|
24
|
+
};
|
|
25
|
+
priority: {
|
|
26
|
+
priority: string;
|
|
27
|
+
color: string;
|
|
28
|
+
} | null;
|
|
29
|
+
list: {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
assignees: Array<{
|
|
34
|
+
id: number;
|
|
35
|
+
username: string;
|
|
36
|
+
email: string;
|
|
37
|
+
}>;
|
|
38
|
+
tags: Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
}>;
|
|
41
|
+
due_date: string | null;
|
|
42
|
+
checklists: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
items: Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
resolved: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
custom_fields: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
type: string;
|
|
53
|
+
value?: unknown;
|
|
54
|
+
type_config?: {
|
|
55
|
+
options?: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
color: string;
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
}>;
|
|
62
|
+
attachments: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
url: string;
|
|
66
|
+
}>;
|
|
67
|
+
linked_tasks: Array<{
|
|
68
|
+
task_id: string;
|
|
69
|
+
link_id: string;
|
|
70
|
+
}>;
|
|
71
|
+
url: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ClickUpComment {
|
|
74
|
+
id: string;
|
|
75
|
+
comment_text: string;
|
|
76
|
+
user: {
|
|
77
|
+
username: string;
|
|
78
|
+
email: string;
|
|
79
|
+
};
|
|
80
|
+
date: string;
|
|
81
|
+
}
|
|
82
|
+
export interface TaskDetails {
|
|
83
|
+
task: ClickUpTask;
|
|
84
|
+
comments: ClickUpComment[];
|
|
85
|
+
}
|
|
86
|
+
export declare class ClickUpClient {
|
|
87
|
+
private apiToken;
|
|
88
|
+
private teamId;
|
|
89
|
+
constructor(apiToken: string, teamId: string);
|
|
90
|
+
/**
|
|
91
|
+
* Make authenticated request to ClickUp API
|
|
92
|
+
*/
|
|
93
|
+
private request;
|
|
94
|
+
/**
|
|
95
|
+
* Test API connection by fetching team info
|
|
96
|
+
*/
|
|
97
|
+
testConnection(): Promise<{
|
|
98
|
+
ok: boolean;
|
|
99
|
+
teamName?: string;
|
|
100
|
+
error?: string;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Get task by ID
|
|
104
|
+
*/
|
|
105
|
+
getTask(taskId: string): Promise<ClickUpTask>;
|
|
106
|
+
/**
|
|
107
|
+
* Get task by custom ID (e.g., ABC-123)
|
|
108
|
+
*/
|
|
109
|
+
getTaskByCustomId(customId: string): Promise<ClickUpTask>;
|
|
110
|
+
/**
|
|
111
|
+
* Get comments for a task
|
|
112
|
+
*/
|
|
113
|
+
getTaskComments(taskId: string): Promise<ClickUpComment[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Get task with all details (including comments)
|
|
116
|
+
*/
|
|
117
|
+
getTaskDetails(taskId: string): Promise<TaskDetails>;
|
|
118
|
+
/**
|
|
119
|
+
* Get task details by custom ID (e.g., ABC-123)
|
|
120
|
+
*/
|
|
121
|
+
getTaskDetailsByCustomId(customId: string): Promise<TaskDetails>;
|
|
122
|
+
/**
|
|
123
|
+
* Get list with available statuses
|
|
124
|
+
*/
|
|
125
|
+
getList(listId: string): Promise<ClickUpList>;
|
|
126
|
+
/**
|
|
127
|
+
* Get available statuses for a task (via its list)
|
|
128
|
+
*/
|
|
129
|
+
getTaskStatuses(taskId: string, isCustomId?: boolean): Promise<ClickUpStatus[]>;
|
|
130
|
+
/**
|
|
131
|
+
* Update task status
|
|
132
|
+
*/
|
|
133
|
+
updateTaskStatus(taskId: string, status: string, isCustomId?: boolean): Promise<ClickUpTask>;
|
|
134
|
+
/**
|
|
135
|
+
* Update custom field value on a task
|
|
136
|
+
*/
|
|
137
|
+
updateCustomField(taskId: string, fieldId: string, value: unknown, isCustomId?: boolean): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Get dropdown custom field options (e.g., for Release field)
|
|
140
|
+
*/
|
|
141
|
+
getDropdownFieldOptions(taskId: string, fieldName: string, isCustomId?: boolean): Promise<{
|
|
142
|
+
fieldId: string;
|
|
143
|
+
options: Array<{
|
|
144
|
+
id: string;
|
|
145
|
+
name: string;
|
|
146
|
+
}>;
|
|
147
|
+
} | null>;
|
|
148
|
+
}
|
package/dist/clickup.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp API client
|
|
3
|
+
*/
|
|
4
|
+
const CLICKUP_API_BASE = "https://api.clickup.com/api/v2";
|
|
5
|
+
export class ClickUpClient {
|
|
6
|
+
apiToken;
|
|
7
|
+
teamId;
|
|
8
|
+
constructor(apiToken, teamId) {
|
|
9
|
+
this.apiToken = apiToken;
|
|
10
|
+
this.teamId = teamId;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Make authenticated request to ClickUp API
|
|
14
|
+
*/
|
|
15
|
+
async request(endpoint, options = {}) {
|
|
16
|
+
const url = `${CLICKUP_API_BASE}${endpoint}`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
...options,
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: this.apiToken,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...options.headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const error = await response.text();
|
|
27
|
+
throw new Error(`ClickUp API error (${response.status}): ${error}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Test API connection by fetching team info
|
|
33
|
+
*/
|
|
34
|
+
async testConnection() {
|
|
35
|
+
try {
|
|
36
|
+
const data = await this.request("/team");
|
|
37
|
+
const team = data.teams.find((t) => t.id === this.teamId);
|
|
38
|
+
if (team) {
|
|
39
|
+
return { ok: true, teamName: team.name };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: `Team ID ${this.teamId} not found. Available teams: ${data.teams.map(t => `${t.name} (${t.id})`).join(", ")}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { ok: false, error: String(error) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get task by ID
|
|
54
|
+
*/
|
|
55
|
+
async getTask(taskId) {
|
|
56
|
+
return this.request(`/task/${taskId}`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get task by custom ID (e.g., ABC-123)
|
|
60
|
+
*/
|
|
61
|
+
async getTaskByCustomId(customId) {
|
|
62
|
+
return this.request(`/task/${customId}?custom_task_ids=true&team_id=${this.teamId}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get comments for a task
|
|
66
|
+
*/
|
|
67
|
+
async getTaskComments(taskId) {
|
|
68
|
+
const data = await this.request(`/task/${taskId}/comment`);
|
|
69
|
+
return data.comments;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get task with all details (including comments)
|
|
73
|
+
*/
|
|
74
|
+
async getTaskDetails(taskId) {
|
|
75
|
+
const [task, comments] = await Promise.all([
|
|
76
|
+
this.getTask(taskId),
|
|
77
|
+
this.getTaskComments(taskId),
|
|
78
|
+
]);
|
|
79
|
+
return { task, comments };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get task details by custom ID (e.g., ABC-123)
|
|
83
|
+
*/
|
|
84
|
+
async getTaskDetailsByCustomId(customId) {
|
|
85
|
+
const task = await this.getTaskByCustomId(customId);
|
|
86
|
+
const comments = await this.getTaskComments(task.id);
|
|
87
|
+
return { task, comments };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get list with available statuses
|
|
91
|
+
*/
|
|
92
|
+
async getList(listId) {
|
|
93
|
+
return this.request(`/list/${listId}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get available statuses for a task (via its list)
|
|
97
|
+
*/
|
|
98
|
+
async getTaskStatuses(taskId, isCustomId = false) {
|
|
99
|
+
const task = isCustomId
|
|
100
|
+
? await this.getTaskByCustomId(taskId)
|
|
101
|
+
: await this.getTask(taskId);
|
|
102
|
+
const list = await this.getList(task.list.id);
|
|
103
|
+
return list.statuses;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Update task status
|
|
107
|
+
*/
|
|
108
|
+
async updateTaskStatus(taskId, status, isCustomId = false) {
|
|
109
|
+
const endpoint = isCustomId
|
|
110
|
+
? `/task/${taskId}?custom_task_ids=true&team_id=${this.teamId}`
|
|
111
|
+
: `/task/${taskId}`;
|
|
112
|
+
return this.request(endpoint, {
|
|
113
|
+
method: "PUT",
|
|
114
|
+
body: JSON.stringify({ status }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Update custom field value on a task
|
|
119
|
+
*/
|
|
120
|
+
async updateCustomField(taskId, fieldId, value, isCustomId = false) {
|
|
121
|
+
const task = isCustomId
|
|
122
|
+
? await this.getTaskByCustomId(taskId)
|
|
123
|
+
: await this.getTask(taskId);
|
|
124
|
+
await this.request(`/task/${task.id}/field/${fieldId}`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify({ value }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get dropdown custom field options (e.g., for Release field)
|
|
131
|
+
*/
|
|
132
|
+
async getDropdownFieldOptions(taskId, fieldName, isCustomId = false) {
|
|
133
|
+
const task = isCustomId
|
|
134
|
+
? await this.getTaskByCustomId(taskId)
|
|
135
|
+
: await this.getTask(taskId);
|
|
136
|
+
const field = task.custom_fields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase() && f.type === "drop_down");
|
|
137
|
+
if (!field || !field.type_config?.options) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
fieldId: field.id,
|
|
142
|
+
options: field.type_config.options.map((o) => ({ id: o.id, name: o.name })),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { ClickUpClient } from "./clickup.js";
|
|
7
|
+
import { parseTaskId, isCustomTaskId } from "./utils.js";
|
|
8
|
+
// Load configuration from environment
|
|
9
|
+
const apiToken = process.env.CLICKUP_API_TOKEN;
|
|
10
|
+
const teamId = process.env.CLICKUP_TEAM_ID;
|
|
11
|
+
if (!apiToken || !teamId) {
|
|
12
|
+
console.error("Error: CLICKUP_API_TOKEN and CLICKUP_TEAM_ID must be set");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Create ClickUp client
|
|
16
|
+
const clickup = new ClickUpClient(apiToken, teamId);
|
|
17
|
+
// Create MCP server instance
|
|
18
|
+
const server = new Server({
|
|
19
|
+
name: "mcp-clickup",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
}, {
|
|
22
|
+
capabilities: {
|
|
23
|
+
tools: {},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Handler for listing available tools
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
28
|
+
return {
|
|
29
|
+
tools: [
|
|
30
|
+
{
|
|
31
|
+
name: "test-connection",
|
|
32
|
+
description: "Test connection to ClickUp API",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {},
|
|
36
|
+
required: [],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "get-task",
|
|
41
|
+
description: "Get task details from ClickUp. Accepts task URL, custom ID (e.g., ABC-123), or task number.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
taskId: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Task identifier: URL, custom ID (ABC-123), or number",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["taskId"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "get-task-statuses",
|
|
55
|
+
description: "Get available statuses for a task. Use this before updating status to see valid options.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
taskId: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Task identifier: URL, custom ID (ABC-123), or number",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["taskId"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "set-task-status",
|
|
69
|
+
description: "Update the status of a ClickUp task. IMPORTANT: Only use with explicit user confirmation!",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
taskId: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Task identifier: URL, custom ID (ABC-123), or number",
|
|
76
|
+
},
|
|
77
|
+
status: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "New status name (must match one of the available statuses)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ["taskId", "status"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "get-release-options",
|
|
87
|
+
description: "Get available Release dropdown options for a task. Use this to see valid values before setting Release.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
taskId: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Task identifier: URL, custom ID (ABC-123), or number",
|
|
94
|
+
},
|
|
95
|
+
fieldName: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Name of the dropdown field (default: 'Release')",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ["taskId"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "set-task-release",
|
|
105
|
+
description: "Set the Release dropdown value on a ClickUp task. IMPORTANT: Only use with explicit user confirmation!",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
taskId: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Task identifier: URL, custom ID (ABC-123), or number",
|
|
112
|
+
},
|
|
113
|
+
release: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Release value to set (must match one of the available options)",
|
|
116
|
+
},
|
|
117
|
+
fieldName: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Name of the dropdown field (default: 'Release')",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["taskId", "release"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// Format task details for display
|
|
129
|
+
function formatTaskDetails(task) {
|
|
130
|
+
const { task: t, comments } = task;
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push(`# ${t.custom_id || t.id} - ${t.name}`);
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push(`**Status:** ${t.status.status}`);
|
|
135
|
+
lines.push(`**Priority:** ${t.priority?.priority || "None"}`);
|
|
136
|
+
lines.push(`**URL:** ${t.url}`);
|
|
137
|
+
if (t.assignees.length > 0) {
|
|
138
|
+
lines.push(`**Assignees:** ${t.assignees.map((a) => a.username).join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
if (t.tags.length > 0) {
|
|
141
|
+
lines.push(`**Tags:** ${t.tags.map((tag) => tag.name).join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
if (t.due_date) {
|
|
144
|
+
lines.push(`**Due:** ${new Date(parseInt(t.due_date)).toLocaleDateString()}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("## Description");
|
|
148
|
+
lines.push(t.description || "(no description)");
|
|
149
|
+
if (t.checklists.length > 0) {
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push("## Checklists");
|
|
152
|
+
for (const checklist of t.checklists) {
|
|
153
|
+
lines.push(`### ${checklist.name}`);
|
|
154
|
+
for (const item of checklist.items) {
|
|
155
|
+
lines.push(`- [${item.resolved ? "x" : " "}] ${item.name}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (t.custom_fields.length > 0) {
|
|
160
|
+
const fieldsWithValues = t.custom_fields.filter((f) => f.value !== undefined && f.value !== null);
|
|
161
|
+
if (fieldsWithValues.length > 0) {
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push("## Custom Fields");
|
|
164
|
+
for (const field of fieldsWithValues) {
|
|
165
|
+
let displayValue;
|
|
166
|
+
if (field.type === "drop_down" && field.type_config?.options) {
|
|
167
|
+
const option = field.type_config.options.find((o, index) => index === field.value || o.id === field.value);
|
|
168
|
+
displayValue = option?.name || String(field.value);
|
|
169
|
+
}
|
|
170
|
+
else if (field.type === "automatic_progress" && typeof field.value === "object") {
|
|
171
|
+
const progress = field.value;
|
|
172
|
+
displayValue = `${progress.percent_complete || 0}%`;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
displayValue = String(field.value);
|
|
176
|
+
}
|
|
177
|
+
lines.push(`- **${field.name}:** ${displayValue}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (t.attachments.length > 0) {
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push("## Attachments");
|
|
184
|
+
for (const attachment of t.attachments) {
|
|
185
|
+
lines.push(`- [${attachment.title}](${attachment.url})`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (comments.length > 0) {
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("## Comments");
|
|
191
|
+
for (const comment of comments) {
|
|
192
|
+
const date = new Date(parseInt(comment.date)).toLocaleString();
|
|
193
|
+
lines.push(`### ${comment.user.username} (${date})`);
|
|
194
|
+
lines.push(comment.comment_text);
|
|
195
|
+
lines.push("");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Add instructions for Claude
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("---");
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push("## Instructions");
|
|
203
|
+
lines.push("1. Analyze this task thoroughly (description, comments, attachments)");
|
|
204
|
+
lines.push("2. Propose an implementation plan with clear steps");
|
|
205
|
+
lines.push("3. **WAIT for user approval** before starting any implementation");
|
|
206
|
+
lines.push("4. Do NOT write any code until the user explicitly approves the plan");
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
|
209
|
+
// Handler for calling tools
|
|
210
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
211
|
+
const { name, arguments: args } = request.params;
|
|
212
|
+
if (name === "test-connection") {
|
|
213
|
+
const result = await clickup.testConnection();
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: result.ok
|
|
219
|
+
? `Connected to ClickUp team: ${result.teamName}`
|
|
220
|
+
: `Connection failed: ${result.error}`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (name === "get-task") {
|
|
226
|
+
const taskId = parseTaskId(args.taskId);
|
|
227
|
+
try {
|
|
228
|
+
let taskDetails;
|
|
229
|
+
if (isCustomTaskId(taskId)) {
|
|
230
|
+
taskDetails = await clickup.getTaskDetailsByCustomId(taskId);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
taskDetails = await clickup.getTaskDetails(taskId);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: formatTaskDetails(taskDetails),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
content: [
|
|
247
|
+
{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: `Error fetching task: ${error}`,
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
isError: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (name === "get-task-statuses") {
|
|
257
|
+
const taskId = parseTaskId(args.taskId);
|
|
258
|
+
try {
|
|
259
|
+
const statuses = await clickup.getTaskStatuses(taskId, isCustomTaskId(taskId));
|
|
260
|
+
const statusList = statuses
|
|
261
|
+
.map((s) => `- ${s.status} (${s.type})`)
|
|
262
|
+
.join("\n");
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `Available statuses for task ${taskId}:\n${statusList}`,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: `Error fetching statuses: ${error}`,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
isError: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (name === "set-task-status") {
|
|
285
|
+
const { taskId: rawTaskId, status } = args;
|
|
286
|
+
const taskId = parseTaskId(rawTaskId);
|
|
287
|
+
try {
|
|
288
|
+
const updatedTask = await clickup.updateTaskStatus(taskId, status, isCustomTaskId(taskId));
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: `Task ${updatedTask.custom_id || updatedTask.id} status updated to: ${updatedTask.status.status}`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: `Error updating status: ${error}`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (name === "get-release-options") {
|
|
311
|
+
const { taskId: rawTaskId, fieldName = "Release" } = args;
|
|
312
|
+
const taskId = parseTaskId(rawTaskId);
|
|
313
|
+
try {
|
|
314
|
+
const fieldData = await clickup.getDropdownFieldOptions(taskId, fieldName, isCustomTaskId(taskId));
|
|
315
|
+
if (!fieldData) {
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `No dropdown field "${fieldName}" found on task ${taskId}`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const optionsList = fieldData.options.map((o) => `- ${o.name}`).join("\n");
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: `Available "${fieldName}" options for task ${taskId}:\n${optionsList}`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `Error fetching field options: ${error}`,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
isError: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (name === "set-task-release") {
|
|
348
|
+
const { taskId: rawTaskId, release, fieldName = "Release" } = args;
|
|
349
|
+
const taskId = parseTaskId(rawTaskId);
|
|
350
|
+
try {
|
|
351
|
+
const fieldData = await clickup.getDropdownFieldOptions(taskId, fieldName, isCustomTaskId(taskId));
|
|
352
|
+
if (!fieldData) {
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: `No dropdown field "${fieldName}" found on task ${taskId}`,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
isError: true,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const option = fieldData.options.find((o) => o.name.toLowerCase() === release.toLowerCase());
|
|
364
|
+
if (!option) {
|
|
365
|
+
const availableOptions = fieldData.options.map((o) => o.name).join(", ");
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: `Invalid release value "${release}". Available options: ${availableOptions}`,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
isError: true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
await clickup.updateCustomField(taskId, fieldData.fieldId, option.id, isCustomTaskId(taskId));
|
|
377
|
+
return {
|
|
378
|
+
content: [
|
|
379
|
+
{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: `Task ${taskId} "${fieldName}" set to: ${option.name}`,
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
return {
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: `Error setting release: ${error}`,
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
isError: true,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
399
|
+
});
|
|
400
|
+
// Start the server
|
|
401
|
+
async function main() {
|
|
402
|
+
const transport = new StdioServerTransport();
|
|
403
|
+
await server.connect(transport);
|
|
404
|
+
console.error("MCP ClickUp server running on stdio");
|
|
405
|
+
}
|
|
406
|
+
main().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { ClickUpClient } from "./clickup.js";
|
|
3
|
+
const apiToken = process.env.CLICKUP_API_TOKEN;
|
|
4
|
+
const teamId = process.env.CLICKUP_TEAM_ID;
|
|
5
|
+
if (!apiToken || !teamId) {
|
|
6
|
+
console.error("Error: CLICKUP_API_TOKEN and CLICKUP_TEAM_ID must be set");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const clickup = new ClickUpClient(apiToken, teamId);
|
|
10
|
+
console.log("Testing ClickUp connection...");
|
|
11
|
+
const result = await clickup.testConnection();
|
|
12
|
+
if (result.ok) {
|
|
13
|
+
console.log(`✓ Connected to team: ${result.teamName}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error(`✗ Connection failed: ${result.error}`);
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { ClickUpClient } from "./clickup.js";
|
|
3
|
+
import { parseTaskId } from "./utils.js";
|
|
4
|
+
const apiToken = process.env.CLICKUP_API_TOKEN;
|
|
5
|
+
const teamId = process.env.CLICKUP_TEAM_ID;
|
|
6
|
+
if (!apiToken || !teamId) {
|
|
7
|
+
console.error("Error: CLICKUP_API_TOKEN and CLICKUP_TEAM_ID must be set");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Get task ID from command line argument
|
|
11
|
+
const taskInput = process.argv[2];
|
|
12
|
+
if (!taskInput) {
|
|
13
|
+
console.error("Usage: node dist/test-get-task.js <task-id>");
|
|
14
|
+
console.error("Examples:");
|
|
15
|
+
console.error(" node dist/test-get-task.js ABC-123");
|
|
16
|
+
console.error(" node dist/test-get-task.js 123");
|
|
17
|
+
console.error(" node dist/test-get-task.js https://app.clickup.com/t/12345/ABC-123");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// Parse task ID from various formats
|
|
21
|
+
const customId = parseTaskId(taskInput);
|
|
22
|
+
const clickup = new ClickUpClient(apiToken, teamId);
|
|
23
|
+
console.log(`Fetching task: ${customId}\n`);
|
|
24
|
+
try {
|
|
25
|
+
const { task, comments } = await clickup.getTaskDetailsByCustomId(customId);
|
|
26
|
+
console.log("=".repeat(60));
|
|
27
|
+
console.log(`TASK: ${task.custom_id || task.id} - ${task.name}`);
|
|
28
|
+
console.log("=".repeat(60));
|
|
29
|
+
console.log(`\nStatus: ${task.status.status}`);
|
|
30
|
+
console.log(`Priority: ${task.priority?.priority || "None"}`);
|
|
31
|
+
console.log(`URL: ${task.url}`);
|
|
32
|
+
if (task.assignees.length > 0) {
|
|
33
|
+
console.log(`Assignees: ${task.assignees.map(a => a.username).join(", ")}`);
|
|
34
|
+
}
|
|
35
|
+
if (task.tags.length > 0) {
|
|
36
|
+
console.log(`Tags: ${task.tags.map(t => t.name).join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
if (task.due_date) {
|
|
39
|
+
console.log(`Due: ${new Date(parseInt(task.due_date)).toLocaleDateString()}`);
|
|
40
|
+
}
|
|
41
|
+
console.log("\n--- DESCRIPTION ---");
|
|
42
|
+
console.log(task.description || "(no description)");
|
|
43
|
+
if (task.checklists.length > 0) {
|
|
44
|
+
console.log("\n--- CHECKLISTS ---");
|
|
45
|
+
for (const checklist of task.checklists) {
|
|
46
|
+
console.log(`\n${checklist.name}:`);
|
|
47
|
+
for (const item of checklist.items) {
|
|
48
|
+
console.log(` [${item.resolved ? "x" : " "}] ${item.name}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (task.custom_fields.length > 0) {
|
|
53
|
+
console.log("\n--- CUSTOM FIELDS ---");
|
|
54
|
+
for (const field of task.custom_fields) {
|
|
55
|
+
if (field.value !== undefined && field.value !== null) {
|
|
56
|
+
let displayValue;
|
|
57
|
+
// Handle dropdown fields - value is orderindex (number)
|
|
58
|
+
if (field.type === "drop_down" && field.type_config?.options) {
|
|
59
|
+
const option = field.type_config.options.find((o, index) => index === field.value || o.id === field.value);
|
|
60
|
+
displayValue = option?.name || String(field.value);
|
|
61
|
+
}
|
|
62
|
+
// Handle automatic progress fields
|
|
63
|
+
else if (field.type === "automatic_progress" && typeof field.value === "object") {
|
|
64
|
+
const progress = field.value;
|
|
65
|
+
displayValue = `${progress.percent_complete || 0}%`;
|
|
66
|
+
}
|
|
67
|
+
// Handle other types
|
|
68
|
+
else {
|
|
69
|
+
displayValue = String(field.value);
|
|
70
|
+
}
|
|
71
|
+
console.log(`${field.name}: ${displayValue}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (task.attachments.length > 0) {
|
|
76
|
+
console.log("\n--- ATTACHMENTS ---");
|
|
77
|
+
for (const attachment of task.attachments) {
|
|
78
|
+
console.log(`- ${attachment.title}: ${attachment.url}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (comments.length > 0) {
|
|
82
|
+
console.log("\n--- COMMENTS ---");
|
|
83
|
+
for (const comment of comments) {
|
|
84
|
+
const date = new Date(parseInt(comment.date)).toLocaleString();
|
|
85
|
+
console.log(`\n[${date}] ${comment.user.username}:`);
|
|
86
|
+
console.log(comment.comment_text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
console.log("\n" + "=".repeat(60));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("Error fetching task:", error);
|
|
93
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for parsing task identifiers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Parse task identifier from various formats:
|
|
6
|
+
* - Full URL: https://app.clickup.com/t/12345/ABC-123
|
|
7
|
+
* - Custom ID: ABC-123
|
|
8
|
+
* - Number only: 123 (requires CLICKUP_TASK_PREFIX to be set)
|
|
9
|
+
*
|
|
10
|
+
* Returns the custom task ID (e.g., "ABC-123") or internal ID
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTaskId(input: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Check if the task ID is a custom ID (ABC-123 format) or internal ID
|
|
15
|
+
*/
|
|
16
|
+
export declare function isCustomTaskId(taskId: string): boolean;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for parsing task identifiers
|
|
3
|
+
*/
|
|
4
|
+
// Task prefix from environment variable (e.g., "PROJ", "TASK", etc.)
|
|
5
|
+
const TASK_PREFIX = process.env.CLICKUP_TASK_PREFIX || "";
|
|
6
|
+
/**
|
|
7
|
+
* Parse task identifier from various formats:
|
|
8
|
+
* - Full URL: https://app.clickup.com/t/12345/ABC-123
|
|
9
|
+
* - Custom ID: ABC-123
|
|
10
|
+
* - Number only: 123 (requires CLICKUP_TASK_PREFIX to be set)
|
|
11
|
+
*
|
|
12
|
+
* Returns the custom task ID (e.g., "ABC-123") or internal ID
|
|
13
|
+
*/
|
|
14
|
+
export function parseTaskId(input) {
|
|
15
|
+
const trimmed = input.trim();
|
|
16
|
+
// Check if it's a URL
|
|
17
|
+
if (trimmed.startsWith("http")) {
|
|
18
|
+
return parseTaskIdFromUrl(trimmed);
|
|
19
|
+
}
|
|
20
|
+
// Check if it already has a prefix (ABC-123 format)
|
|
21
|
+
if (/^[A-Za-z]+-\d+$/.test(trimmed)) {
|
|
22
|
+
return trimmed.toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
// Check if it's just a number and we have a prefix configured
|
|
25
|
+
if (/^\d+$/.test(trimmed) && TASK_PREFIX) {
|
|
26
|
+
return `${TASK_PREFIX}-${trimmed}`;
|
|
27
|
+
}
|
|
28
|
+
// Return as-is if we can't parse it (could be internal ID)
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract task ID from ClickUp URL
|
|
33
|
+
* Supports formats:
|
|
34
|
+
* - https://app.clickup.com/t/12345/ABC-123 (with custom ID)
|
|
35
|
+
* - https://app.clickup.com/t/abc123xyz (internal ID only)
|
|
36
|
+
*/
|
|
37
|
+
function parseTaskIdFromUrl(url) {
|
|
38
|
+
try {
|
|
39
|
+
const urlObj = new URL(url);
|
|
40
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
41
|
+
// URL format: /t/{team_id_or_task_id}/{custom_id}
|
|
42
|
+
// or: /t/{internal_task_id}
|
|
43
|
+
if (pathParts[0] === "t" && pathParts.length >= 2) {
|
|
44
|
+
// If there's a third part, it's likely the custom ID
|
|
45
|
+
if (pathParts.length >= 3) {
|
|
46
|
+
const customId = pathParts[2];
|
|
47
|
+
// Check if it looks like a custom ID (has prefix)
|
|
48
|
+
if (customId.includes("-")) {
|
|
49
|
+
return customId.toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Otherwise return the second part (could be internal task ID)
|
|
53
|
+
return pathParts[1];
|
|
54
|
+
}
|
|
55
|
+
throw new Error("Could not parse task ID from URL");
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error(`Invalid ClickUp URL: ${url}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if the task ID is a custom ID (ABC-123 format) or internal ID
|
|
63
|
+
*/
|
|
64
|
+
export function isCustomTaskId(taskId) {
|
|
65
|
+
return /^[A-Za-z]+-\d+$/.test(taskId);
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@d4works/mcp-clickup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for ClickUp task management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-clickup": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"prepare": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"clickup",
|
|
19
|
+
"claude",
|
|
20
|
+
"ai",
|
|
21
|
+
"task-management",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
+
"dotenv": "^17.2.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|