@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 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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
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
+ }
@@ -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
+ }