Skip to main content

clickup_cli/
mcp.rs

1use crate::client::ClickUpClient;
2use crate::config::Config;
3use crate::output::compact_items;
4use serde_json::{json, Value};
5use tokio::io::{AsyncBufReadExt, BufReader};
6
7// ── JSON-RPC helpers ──────────────────────────────────────────────────────────
8
9fn ok_response(id: &Value, result: Value) -> Value {
10    json!({"jsonrpc":"2.0","id":id,"result":result})
11}
12
13fn error_response(id: &Value, code: i64, message: &str) -> Value {
14    json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message}})
15}
16
17fn tool_result(text: String) -> Value {
18    json!({"content":[{"type":"text","text":text}]})
19}
20
21fn tool_error(msg: String) -> Value {
22    json!({"content":[{"type":"text","text":msg}],"isError":true})
23}
24
25// ── Tool definitions ──────────────────────────────────────────────────────────
26
27fn tool_list() -> Value {
28    json!([
29        {
30            "name": "clickup_whoami",
31            "description": "Get the currently authenticated ClickUp user",
32            "inputSchema": {
33                "type": "object",
34                "properties": {},
35                "required": []
36            }
37        },
38        {
39            "name": "clickup_workspace_list",
40            "description": "List all ClickUp workspaces (teams) accessible to the current user",
41            "inputSchema": {
42                "type": "object",
43                "properties": {},
44                "required": []
45            }
46        },
47        {
48            "name": "clickup_space_list",
49            "description": "List spaces in a workspace",
50            "inputSchema": {
51                "type": "object",
52                "properties": {
53                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
54                    "archived": {"type": "boolean", "description": "Include archived spaces"}
55                },
56                "required": []
57            }
58        },
59        {
60            "name": "clickup_folder_list",
61            "description": "List folders in a space",
62            "inputSchema": {
63                "type": "object",
64                "properties": {
65                    "space_id": {"type": "string", "description": "Space ID"},
66                    "archived": {"type": "boolean", "description": "Include archived folders"}
67                },
68                "required": ["space_id"]
69            }
70        },
71        {
72            "name": "clickup_list_list",
73            "description": "List ClickUp lists in a folder or space (folderless lists)",
74            "inputSchema": {
75                "type": "object",
76                "properties": {
77                    "folder_id": {"type": "string", "description": "Folder ID (mutually exclusive with space_id)"},
78                    "space_id": {"type": "string", "description": "Space ID for folderless lists (mutually exclusive with folder_id)"},
79                    "archived": {"type": "boolean", "description": "Include archived lists"}
80                },
81                "required": []
82            }
83        },
84        {
85            "name": "clickup_task_list",
86            "description": "List tasks in a ClickUp list",
87            "inputSchema": {
88                "type": "object",
89                "properties": {
90                    "list_id": {"type": "string", "description": "List ID"},
91                    "statuses": {
92                        "type": "array",
93                        "items": {"type": "string"},
94                        "description": "Filter by status names"
95                    },
96                    "assignees": {
97                        "type": "array",
98                        "items": {"type": "string"},
99                        "description": "Filter by assignee user IDs"
100                    },
101                    "include_closed": {"type": "boolean", "description": "Include closed tasks"}
102                },
103                "required": ["list_id"]
104            }
105        },
106        {
107            "name": "clickup_task_get",
108            "description": "Get details of a specific ClickUp task",
109            "inputSchema": {
110                "type": "object",
111                "properties": {
112                    "task_id": {"type": "string", "description": "Task ID"},
113                    "include_subtasks": {"type": "boolean", "description": "Include subtasks in the response"}
114                },
115                "required": ["task_id"]
116            }
117        },
118        {
119            "name": "clickup_task_create",
120            "description": "Create a new task in a ClickUp list",
121            "inputSchema": {
122                "type": "object",
123                "properties": {
124                    "list_id": {"type": "string", "description": "List ID to create the task in"},
125                    "name": {"type": "string", "description": "Task name"},
126                    "description": {"type": "string", "description": "Task description (markdown supported)"},
127                    "status": {"type": "string", "description": "Task status"},
128                    "priority": {"type": "integer", "description": "Priority (1=urgent, 2=high, 3=normal, 4=low)"},
129                    "assignees": {
130                        "type": "array",
131                        "items": {"type": "integer"},
132                        "description": "List of assignee user IDs"
133                    },
134                    "tags": {
135                        "type": "array",
136                        "items": {"type": "string"},
137                        "description": "List of tag names"
138                    },
139                    "due_date": {"type": "integer", "description": "Due date as Unix timestamp (milliseconds)"}
140                },
141                "required": ["list_id", "name"]
142            }
143        },
144        {
145            "name": "clickup_task_update",
146            "description": "Update an existing ClickUp task",
147            "inputSchema": {
148                "type": "object",
149                "properties": {
150                    "task_id": {"type": "string", "description": "Task ID"},
151                    "name": {"type": "string", "description": "New task name"},
152                    "status": {"type": "string", "description": "New status"},
153                    "priority": {"type": "integer", "description": "New priority (1=urgent, 2=high, 3=normal, 4=low)"},
154                    "description": {"type": "string", "description": "New description"},
155                    "add_assignees": {
156                        "type": "array",
157                        "items": {"type": "integer"},
158                        "description": "User IDs to add as assignees"
159                    },
160                    "rem_assignees": {
161                        "type": "array",
162                        "items": {"type": "integer"},
163                        "description": "User IDs to remove from assignees"
164                    }
165                },
166                "required": ["task_id"]
167            }
168        },
169        {
170            "name": "clickup_task_delete",
171            "description": "Delete a ClickUp task",
172            "inputSchema": {
173                "type": "object",
174                "properties": {
175                    "task_id": {"type": "string", "description": "Task ID"}
176                },
177                "required": ["task_id"]
178            }
179        },
180        {
181            "name": "clickup_task_search",
182            "description": "Search tasks across a workspace with optional filters",
183            "inputSchema": {
184                "type": "object",
185                "properties": {
186                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
187                    "space_ids": {
188                        "type": "array",
189                        "items": {"type": "string"},
190                        "description": "Filter by space IDs"
191                    },
192                    "list_ids": {
193                        "type": "array",
194                        "items": {"type": "string"},
195                        "description": "Filter by list IDs"
196                    },
197                    "statuses": {
198                        "type": "array",
199                        "items": {"type": "string"},
200                        "description": "Filter by status names"
201                    },
202                    "assignees": {
203                        "type": "array",
204                        "items": {"type": "string"},
205                        "description": "Filter by assignee user IDs"
206                    }
207                },
208                "required": []
209            }
210        },
211        {
212            "name": "clickup_comment_list",
213            "description": "List comments on a ClickUp task",
214            "inputSchema": {
215                "type": "object",
216                "properties": {
217                    "task_id": {"type": "string", "description": "Task ID"}
218                },
219                "required": ["task_id"]
220            }
221        },
222        {
223            "name": "clickup_comment_create",
224            "description": "Create a comment on a ClickUp task",
225            "inputSchema": {
226                "type": "object",
227                "properties": {
228                    "task_id": {"type": "string", "description": "Task ID"},
229                    "text": {"type": "string", "description": "Comment text"},
230                    "assignee": {"type": "integer", "description": "Assign the comment to a user ID"},
231                    "notify_all": {"type": "boolean", "description": "Notify all assignees"}
232                },
233                "required": ["task_id", "text"]
234            }
235        },
236        {
237            "name": "clickup_field_list",
238            "description": "List custom fields for a ClickUp list",
239            "inputSchema": {
240                "type": "object",
241                "properties": {
242                    "list_id": {"type": "string", "description": "List ID"}
243                },
244                "required": ["list_id"]
245            }
246        },
247        {
248            "name": "clickup_field_set",
249            "description": "Set a custom field value on a ClickUp task",
250            "inputSchema": {
251                "type": "object",
252                "properties": {
253                    "task_id": {"type": "string", "description": "Task ID"},
254                    "field_id": {"type": "string", "description": "Custom field ID"},
255                    "value": {"description": "Value to set (type depends on the custom field type)"}
256                },
257                "required": ["task_id", "field_id", "value"]
258            }
259        },
260        {
261            "name": "clickup_time_start",
262            "description": "Start a time tracking entry for a task",
263            "inputSchema": {
264                "type": "object",
265                "properties": {
266                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
267                    "task_id": {"type": "string", "description": "Task ID to track time against"},
268                    "description": {"type": "string", "description": "Description of the time entry"},
269                    "billable": {"type": "boolean", "description": "Whether this entry is billable"}
270                },
271                "required": []
272            }
273        },
274        {
275            "name": "clickup_time_stop",
276            "description": "Stop the currently running time tracking entry",
277            "inputSchema": {
278                "type": "object",
279                "properties": {
280                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."}
281                },
282                "required": []
283            }
284        },
285        {
286            "name": "clickup_time_list",
287            "description": "List time tracking entries for a workspace",
288            "inputSchema": {
289                "type": "object",
290                "properties": {
291                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
292                    "start_date": {"type": "integer", "description": "Start date as Unix timestamp (milliseconds)"},
293                    "end_date": {"type": "integer", "description": "End date as Unix timestamp (milliseconds)"},
294                    "task_id": {"type": "string", "description": "Filter by task ID"}
295                },
296                "required": []
297            }
298        },
299        {
300            "name": "clickup_checklist_create",
301            "description": "Create a checklist on a ClickUp task",
302            "inputSchema": {
303                "type": "object",
304                "properties": {
305                    "task_id": {"type": "string", "description": "Task ID"},
306                    "name": {"type": "string", "description": "Checklist name"}
307                },
308                "required": ["task_id", "name"]
309            }
310        },
311        {
312            "name": "clickup_checklist_delete",
313            "description": "Delete a checklist from a ClickUp task",
314            "inputSchema": {
315                "type": "object",
316                "properties": {
317                    "checklist_id": {"type": "string", "description": "Checklist ID"}
318                },
319                "required": ["checklist_id"]
320            }
321        },
322        {
323            "name": "clickup_goal_list",
324            "description": "List goals in a workspace",
325            "inputSchema": {
326                "type": "object",
327                "properties": {
328                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."}
329                },
330                "required": []
331            }
332        },
333        {
334            "name": "clickup_goal_get",
335            "description": "Get details of a specific goal",
336            "inputSchema": {
337                "type": "object",
338                "properties": {
339                    "goal_id": {"type": "string", "description": "Goal ID"}
340                },
341                "required": ["goal_id"]
342            }
343        },
344        {
345            "name": "clickup_goal_create",
346            "description": "Create a new goal in a workspace",
347            "inputSchema": {
348                "type": "object",
349                "properties": {
350                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
351                    "name": {"type": "string", "description": "Goal name"},
352                    "due_date": {"type": "integer", "description": "Due date as Unix timestamp (milliseconds)"},
353                    "description": {"type": "string", "description": "Goal description"},
354                    "owner_ids": {
355                        "type": "array",
356                        "items": {"type": "integer"},
357                        "description": "List of owner user IDs"
358                    }
359                },
360                "required": ["name"]
361            }
362        },
363        {
364            "name": "clickup_goal_update",
365            "description": "Update an existing goal",
366            "inputSchema": {
367                "type": "object",
368                "properties": {
369                    "goal_id": {"type": "string", "description": "Goal ID"},
370                    "name": {"type": "string", "description": "New goal name"},
371                    "due_date": {"type": "integer", "description": "New due date as Unix timestamp (milliseconds)"},
372                    "description": {"type": "string", "description": "New goal description"}
373                },
374                "required": ["goal_id"]
375            }
376        },
377        {
378            "name": "clickup_view_list",
379            "description": "List views for a space, folder, list, or workspace",
380            "inputSchema": {
381                "type": "object",
382                "properties": {
383                    "space_id": {"type": "string", "description": "Space ID (mutually exclusive with other IDs)"},
384                    "folder_id": {"type": "string", "description": "Folder ID (mutually exclusive with other IDs)"},
385                    "list_id": {"type": "string", "description": "List ID (mutually exclusive with other IDs)"},
386                    "team_id": {"type": "string", "description": "Workspace (team) ID for workspace-level views. Omit to use the default workspace from config."}
387                },
388                "required": []
389            }
390        },
391        {
392            "name": "clickup_view_tasks",
393            "description": "Get tasks in a specific view",
394            "inputSchema": {
395                "type": "object",
396                "properties": {
397                    "view_id": {"type": "string", "description": "View ID"},
398                    "page": {"type": "integer", "description": "Page number (0-indexed, default 0)"}
399                },
400                "required": ["view_id"]
401            }
402        },
403        {
404            "name": "clickup_doc_list",
405            "description": "List docs in a workspace",
406            "inputSchema": {
407                "type": "object",
408                "properties": {
409                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."}
410                },
411                "required": []
412            }
413        },
414        {
415            "name": "clickup_doc_get",
416            "description": "Get a specific doc from a workspace",
417            "inputSchema": {
418                "type": "object",
419                "properties": {
420                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
421                    "doc_id": {"type": "string", "description": "Doc ID"}
422                },
423                "required": ["doc_id"]
424            }
425        },
426        {
427            "name": "clickup_doc_pages",
428            "description": "List pages in a doc",
429            "inputSchema": {
430                "type": "object",
431                "properties": {
432                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
433                    "doc_id": {"type": "string", "description": "Doc ID"},
434                    "content": {"type": "boolean", "description": "Include page content in the response"}
435                },
436                "required": ["doc_id"]
437            }
438        },
439        {
440            "name": "clickup_tag_list",
441            "description": "List tags for a space",
442            "inputSchema": {
443                "type": "object",
444                "properties": {
445                    "space_id": {"type": "string", "description": "Space ID"}
446                },
447                "required": ["space_id"]
448            }
449        },
450        {
451            "name": "clickup_task_add_tag",
452            "description": "Add a tag to a ClickUp task",
453            "inputSchema": {
454                "type": "object",
455                "properties": {
456                    "task_id": {"type": "string", "description": "Task ID"},
457                    "tag_name": {"type": "string", "description": "Tag name to add"}
458                },
459                "required": ["task_id", "tag_name"]
460            }
461        },
462        {
463            "name": "clickup_task_remove_tag",
464            "description": "Remove a tag from a ClickUp task",
465            "inputSchema": {
466                "type": "object",
467                "properties": {
468                    "task_id": {"type": "string", "description": "Task ID"},
469                    "tag_name": {"type": "string", "description": "Tag name to remove"}
470                },
471                "required": ["task_id", "tag_name"]
472            }
473        },
474        {
475            "name": "clickup_webhook_list",
476            "description": "List webhooks for a workspace",
477            "inputSchema": {
478                "type": "object",
479                "properties": {
480                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."}
481                },
482                "required": []
483            }
484        },
485        {
486            "name": "clickup_member_list",
487            "description": "List members of a task or list",
488            "inputSchema": {
489                "type": "object",
490                "properties": {
491                    "task_id": {"type": "string", "description": "Task ID (mutually exclusive with list_id)"},
492                    "list_id": {"type": "string", "description": "List ID (mutually exclusive with task_id)"}
493                },
494                "required": []
495            }
496        },
497        {
498            "name": "clickup_template_list",
499            "description": "List task templates for a workspace",
500            "inputSchema": {
501                "type": "object",
502                "properties": {
503                    "team_id": {"type": "string", "description": "Workspace (team) ID. Omit to use the default workspace from config."},
504                    "page": {"type": "integer", "description": "Page number (0-indexed, default 0)"}
505                },
506                "required": []
507            }
508        }
509    ])
510}
511
512// ── Tool execution ────────────────────────────────────────────────────────────
513
514async fn call_tool(
515    name: &str,
516    args: &Value,
517    client: &ClickUpClient,
518    workspace_id: &Option<String>,
519) -> Value {
520    let result = dispatch_tool(name, args, client, workspace_id).await;
521    match result {
522        Ok(v) => tool_result(v.to_string()),
523        Err(e) => tool_error(format!("Error: {}", e)),
524    }
525}
526
527async fn dispatch_tool(
528    name: &str,
529    args: &Value,
530    client: &ClickUpClient,
531    workspace_id: &Option<String>,
532) -> Result<Value, String> {
533    let empty = json!({});
534    let args = if args.is_null() { &empty } else { args };
535
536    // Resolve workspace ID from args or config
537    let resolve_workspace = |args: &Value| -> Result<String, String> {
538        if let Some(id) = args.get("team_id").and_then(|v| v.as_str()) {
539            return Ok(id.to_string());
540        }
541        workspace_id
542            .clone()
543            .ok_or_else(|| "No workspace_id found in config. Please run `clickup setup` or provide team_id in the tool arguments.".to_string())
544    };
545
546    match name {
547        "clickup_whoami" => {
548            let resp = client.get("/v2/user").await.map_err(|e| e.to_string())?;
549            let user = resp.get("user").cloned().unwrap_or(resp);
550            Ok(compact_items(&[user], &["id", "username", "email"]))
551        }
552
553        "clickup_workspace_list" => {
554            let resp = client.get("/v2/team").await.map_err(|e| e.to_string())?;
555            let teams = resp.get("teams").and_then(|t| t.as_array()).cloned().unwrap_or_default();
556            let items: Vec<Value> = teams.iter().map(|ws| {
557                json!({
558                    "id": ws.get("id"),
559                    "name": ws.get("name"),
560                    "members": ws.get("members").and_then(|m| m.as_array()).map(|a| a.len()).unwrap_or(0),
561                })
562            }).collect();
563            Ok(compact_items(&items, &["id", "name", "members"]))
564        }
565
566        "clickup_space_list" => {
567            let team_id = resolve_workspace(args)?;
568            let archived = args.get("archived").and_then(|v| v.as_bool()).unwrap_or(false);
569            let path = format!("/v2/team/{}/space?archived={}", team_id, archived);
570            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
571            let spaces = resp.get("spaces").and_then(|s| s.as_array()).cloned().unwrap_or_default();
572            Ok(compact_items(&spaces, &["id", "name", "private", "archived"]))
573        }
574
575        "clickup_folder_list" => {
576            let space_id = args
577                .get("space_id")
578                .and_then(|v| v.as_str())
579                .ok_or("Missing required parameter: space_id")?;
580            let archived = args.get("archived").and_then(|v| v.as_bool()).unwrap_or(false);
581            let path = format!("/v2/space/{}/folder?archived={}", space_id, archived);
582            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
583            let folders = resp.get("folders").and_then(|f| f.as_array()).cloned().unwrap_or_default();
584            let items: Vec<Value> = folders.iter().map(|f| {
585                let list_count = f.get("lists").and_then(|l| l.as_array()).map(|a| a.len()).unwrap_or(0);
586                json!({
587                    "id": f.get("id"),
588                    "name": f.get("name"),
589                    "task_count": f.get("task_count"),
590                    "list_count": list_count,
591                })
592            }).collect();
593            Ok(compact_items(&items, &["id", "name", "task_count", "list_count"]))
594        }
595
596        "clickup_list_list" => {
597            let archived = args.get("archived").and_then(|v| v.as_bool()).unwrap_or(false);
598            let path = if let Some(folder_id) = args.get("folder_id").and_then(|v| v.as_str()) {
599                format!("/v2/folder/{}/list?archived={}", folder_id, archived)
600            } else if let Some(space_id) = args.get("space_id").and_then(|v| v.as_str()) {
601                format!("/v2/space/{}/list?archived={}", space_id, archived)
602            } else {
603                return Err("Provide either folder_id or space_id".to_string());
604            };
605            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
606            let lists = resp.get("lists").and_then(|l| l.as_array()).cloned().unwrap_or_default();
607            Ok(compact_items(&lists, &["id", "name", "task_count", "status", "due_date"]))
608        }
609
610        "clickup_task_list" => {
611            let list_id = args
612                .get("list_id")
613                .and_then(|v| v.as_str())
614                .ok_or("Missing required parameter: list_id")?;
615            let mut qs = String::new();
616            if let Some(include_closed) = args.get("include_closed").and_then(|v| v.as_bool()) {
617                qs.push_str(&format!("&include_closed={}", include_closed));
618            }
619            if let Some(statuses) = args.get("statuses").and_then(|v| v.as_array()) {
620                for s in statuses {
621                    if let Some(s) = s.as_str() {
622                        qs.push_str(&format!("&statuses[]={}", s));
623                    }
624                }
625            }
626            if let Some(assignees) = args.get("assignees").and_then(|v| v.as_array()) {
627                for a in assignees {
628                    if let Some(a) = a.as_str() {
629                        qs.push_str(&format!("&assignees[]={}", a));
630                    }
631                }
632            }
633            let path = format!("/v2/list/{}/task?{}", list_id, qs.trim_start_matches('&'));
634            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
635            let tasks = resp.get("tasks").and_then(|t| t.as_array()).cloned().unwrap_or_default();
636            Ok(compact_items(&tasks, &["id", "name", "status", "priority", "assignees", "due_date"]))
637        }
638
639        "clickup_task_get" => {
640            let task_id = args
641                .get("task_id")
642                .and_then(|v| v.as_str())
643                .ok_or("Missing required parameter: task_id")?;
644            let include_subtasks = args
645                .get("include_subtasks")
646                .and_then(|v| v.as_bool())
647                .unwrap_or(false);
648            let path = format!(
649                "/v2/task/{}?include_subtasks={}",
650                task_id, include_subtasks
651            );
652            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
653            Ok(compact_items(&[resp], &["id", "name", "status", "priority", "assignees", "due_date", "description"]))
654        }
655
656        "clickup_task_create" => {
657            let list_id = args
658                .get("list_id")
659                .and_then(|v| v.as_str())
660                .ok_or("Missing required parameter: list_id")?;
661            let name = args
662                .get("name")
663                .and_then(|v| v.as_str())
664                .ok_or("Missing required parameter: name")?;
665            let mut body = json!({"name": name});
666            if let Some(desc) = args.get("description").and_then(|v| v.as_str()) {
667                body["description"] = json!(desc);
668            }
669            if let Some(status) = args.get("status").and_then(|v| v.as_str()) {
670                body["status"] = json!(status);
671            }
672            if let Some(priority) = args.get("priority").and_then(|v| v.as_i64()) {
673                body["priority"] = json!(priority);
674            }
675            if let Some(assignees) = args.get("assignees") {
676                body["assignees"] = assignees.clone();
677            }
678            if let Some(tags) = args.get("tags") {
679                body["tags"] = tags.clone();
680            }
681            if let Some(due_date) = args.get("due_date").and_then(|v| v.as_i64()) {
682                body["due_date"] = json!(due_date);
683            }
684            let path = format!("/v2/list/{}/task", list_id);
685            let resp = client.post(&path, &body).await.map_err(|e| e.to_string())?;
686            Ok(compact_items(&[resp], &["id", "name", "status", "priority", "assignees", "due_date"]))
687        }
688
689        "clickup_task_update" => {
690            let task_id = args
691                .get("task_id")
692                .and_then(|v| v.as_str())
693                .ok_or("Missing required parameter: task_id")?;
694            let mut body = json!({});
695            if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
696                body["name"] = json!(name);
697            }
698            if let Some(status) = args.get("status").and_then(|v| v.as_str()) {
699                body["status"] = json!(status);
700            }
701            if let Some(priority) = args.get("priority").and_then(|v| v.as_i64()) {
702                body["priority"] = json!(priority);
703            }
704            if let Some(desc) = args.get("description").and_then(|v| v.as_str()) {
705                body["description"] = json!(desc);
706            }
707            if let Some(add) = args.get("add_assignees") {
708                body["assignees"] = json!({"add": add, "rem": args.get("rem_assignees").cloned().unwrap_or(json!([]))});
709            } else if let Some(rem) = args.get("rem_assignees") {
710                body["assignees"] = json!({"add": [], "rem": rem});
711            }
712            let path = format!("/v2/task/{}", task_id);
713            let resp = client.put(&path, &body).await.map_err(|e| e.to_string())?;
714            Ok(compact_items(&[resp], &["id", "name", "status", "priority", "assignees", "due_date"]))
715        }
716
717        "clickup_task_delete" => {
718            let task_id = args
719                .get("task_id")
720                .and_then(|v| v.as_str())
721                .ok_or("Missing required parameter: task_id")?;
722            let path = format!("/v2/task/{}", task_id);
723            client.delete(&path).await.map_err(|e| e.to_string())?;
724            Ok(json!({"message": format!("Task {} deleted", task_id)}))
725        }
726
727        "clickup_task_search" => {
728            let team_id = resolve_workspace(args)?;
729            let mut qs = String::new();
730            if let Some(space_ids) = args.get("space_ids").and_then(|v| v.as_array()) {
731                for id in space_ids {
732                    if let Some(id) = id.as_str() {
733                        qs.push_str(&format!("&space_ids[]={}", id));
734                    }
735                }
736            }
737            if let Some(list_ids) = args.get("list_ids").and_then(|v| v.as_array()) {
738                for id in list_ids {
739                    if let Some(id) = id.as_str() {
740                        qs.push_str(&format!("&list_ids[]={}", id));
741                    }
742                }
743            }
744            if let Some(statuses) = args.get("statuses").and_then(|v| v.as_array()) {
745                for s in statuses {
746                    if let Some(s) = s.as_str() {
747                        qs.push_str(&format!("&statuses[]={}", s));
748                    }
749                }
750            }
751            if let Some(assignees) = args.get("assignees").and_then(|v| v.as_array()) {
752                for a in assignees {
753                    if let Some(a) = a.as_str() {
754                        qs.push_str(&format!("&assignees[]={}", a));
755                    }
756                }
757            }
758            let path = format!(
759                "/v2/team/{}/task?{}",
760                team_id,
761                qs.trim_start_matches('&')
762            );
763            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
764            let tasks = resp.get("tasks").and_then(|t| t.as_array()).cloned().unwrap_or_default();
765            Ok(compact_items(&tasks, &["id", "name", "status", "priority", "assignees", "due_date"]))
766        }
767
768        "clickup_comment_list" => {
769            let task_id = args
770                .get("task_id")
771                .and_then(|v| v.as_str())
772                .ok_or("Missing required parameter: task_id")?;
773            let path = format!("/v2/task/{}/comment", task_id);
774            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
775            let comments = resp.get("comments").and_then(|c| c.as_array()).cloned().unwrap_or_default();
776            Ok(compact_items(&comments, &["id", "user", "date", "comment_text"]))
777        }
778
779        "clickup_comment_create" => {
780            let task_id = args
781                .get("task_id")
782                .and_then(|v| v.as_str())
783                .ok_or("Missing required parameter: task_id")?;
784            let text = args
785                .get("text")
786                .and_then(|v| v.as_str())
787                .ok_or("Missing required parameter: text")?;
788            let mut body = json!({"comment_text": text});
789            if let Some(assignee) = args.get("assignee").and_then(|v| v.as_i64()) {
790                body["assignee"] = json!(assignee);
791            }
792            if let Some(notify_all) = args.get("notify_all").and_then(|v| v.as_bool()) {
793                body["notify_all"] = json!(notify_all);
794            }
795            let path = format!("/v2/task/{}/comment", task_id);
796            let resp = client.post(&path, &body).await.map_err(|e| e.to_string())?;
797            Ok(json!({"message": "Comment created", "id": resp.get("id")}))
798        }
799
800        "clickup_field_list" => {
801            let list_id = args
802                .get("list_id")
803                .and_then(|v| v.as_str())
804                .ok_or("Missing required parameter: list_id")?;
805            let path = format!("/v2/list/{}/field", list_id);
806            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
807            let fields = resp.get("fields").and_then(|f| f.as_array()).cloned().unwrap_or_default();
808            Ok(compact_items(&fields, &["id", "name", "type", "required"]))
809        }
810
811        "clickup_field_set" => {
812            let task_id = args
813                .get("task_id")
814                .and_then(|v| v.as_str())
815                .ok_or("Missing required parameter: task_id")?;
816            let field_id = args
817                .get("field_id")
818                .and_then(|v| v.as_str())
819                .ok_or("Missing required parameter: field_id")?;
820            let value = args.get("value").ok_or("Missing required parameter: value")?;
821            let body = json!({"value": value});
822            let path = format!("/v2/task/{}/field/{}", task_id, field_id);
823            client.post(&path, &body).await.map_err(|e| e.to_string())?;
824            Ok(json!({"message": format!("Field {} set on task {}", field_id, task_id)}))
825        }
826
827        "clickup_time_start" => {
828            let team_id = resolve_workspace(args)?;
829            let mut body = json!({});
830            if let Some(task_id) = args.get("task_id").and_then(|v| v.as_str()) {
831                body["tid"] = json!(task_id);
832            }
833            if let Some(desc) = args.get("description").and_then(|v| v.as_str()) {
834                body["description"] = json!(desc);
835            }
836            if let Some(billable) = args.get("billable").and_then(|v| v.as_bool()) {
837                body["billable"] = json!(billable);
838            }
839            let path = format!("/v2/team/{}/time_entries/start", team_id);
840            let resp = client.post(&path, &body).await.map_err(|e| e.to_string())?;
841            let data = resp.get("data").cloned().unwrap_or(resp);
842            Ok(compact_items(&[data], &["id", "task", "duration", "start", "billable"]))
843        }
844
845        "clickup_time_stop" => {
846            let team_id = resolve_workspace(args)?;
847            let path = format!("/v2/team/{}/time_entries/stop", team_id);
848            let resp = client.post(&path, &json!({})).await.map_err(|e| e.to_string())?;
849            let data = resp.get("data").cloned().unwrap_or(resp);
850            Ok(compact_items(&[data], &["id", "task", "duration", "start", "end", "billable"]))
851        }
852
853        "clickup_time_list" => {
854            let team_id = resolve_workspace(args)?;
855            let mut qs = String::new();
856            if let Some(start_date) = args.get("start_date").and_then(|v| v.as_i64()) {
857                qs.push_str(&format!("&start_date={}", start_date));
858            }
859            if let Some(end_date) = args.get("end_date").and_then(|v| v.as_i64()) {
860                qs.push_str(&format!("&end_date={}", end_date));
861            }
862            if let Some(task_id) = args.get("task_id").and_then(|v| v.as_str()) {
863                qs.push_str(&format!("&task_id={}", task_id));
864            }
865            let path = format!(
866                "/v2/team/{}/time_entries?{}",
867                team_id,
868                qs.trim_start_matches('&')
869            );
870            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
871            let entries = resp.get("data").and_then(|d| d.as_array()).cloned().unwrap_or_default();
872            Ok(compact_items(&entries, &["id", "task", "duration", "start", "billable"]))
873        }
874
875        "clickup_checklist_create" => {
876            let task_id = args
877                .get("task_id")
878                .and_then(|v| v.as_str())
879                .ok_or("Missing required parameter: task_id")?;
880            let name = args
881                .get("name")
882                .and_then(|v| v.as_str())
883                .ok_or("Missing required parameter: name")?;
884            let path = format!("/v2/task/{}/checklist", task_id);
885            let body = json!({"name": name});
886            let resp = client.post(&path, &body).await.map_err(|e| e.to_string())?;
887            let checklist = resp.get("checklist").cloned().unwrap_or(resp);
888            Ok(compact_items(&[checklist], &["id", "name"]))
889        }
890
891        "clickup_checklist_delete" => {
892            let checklist_id = args
893                .get("checklist_id")
894                .and_then(|v| v.as_str())
895                .ok_or("Missing required parameter: checklist_id")?;
896            let path = format!("/v2/checklist/{}", checklist_id);
897            client.delete(&path).await.map_err(|e| e.to_string())?;
898            Ok(json!({"message": format!("Checklist {} deleted", checklist_id)}))
899        }
900
901        "clickup_goal_list" => {
902            let team_id = resolve_workspace(args)?;
903            let path = format!("/v2/team/{}/goal", team_id);
904            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
905            let goals = resp.get("goals").and_then(|g| g.as_array()).cloned().unwrap_or_default();
906            Ok(compact_items(&goals, &["id", "name", "percent_completed", "due_date"]))
907        }
908
909        "clickup_goal_get" => {
910            let goal_id = args
911                .get("goal_id")
912                .and_then(|v| v.as_str())
913                .ok_or("Missing required parameter: goal_id")?;
914            let path = format!("/v2/goal/{}", goal_id);
915            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
916            let goal = resp.get("goal").cloned().unwrap_or(resp);
917            Ok(compact_items(&[goal], &["id", "name", "percent_completed", "due_date", "description"]))
918        }
919
920        "clickup_goal_create" => {
921            let team_id = resolve_workspace(args)?;
922            let name = args
923                .get("name")
924                .and_then(|v| v.as_str())
925                .ok_or("Missing required parameter: name")?;
926            let mut body = json!({"name": name});
927            if let Some(due_date) = args.get("due_date").and_then(|v| v.as_i64()) {
928                body["due_date"] = json!(due_date);
929            }
930            if let Some(desc) = args.get("description").and_then(|v| v.as_str()) {
931                body["description"] = json!(desc);
932            }
933            if let Some(owner_ids) = args.get("owner_ids") {
934                body["owners"] = owner_ids.clone();
935            }
936            let path = format!("/v2/team/{}/goal", team_id);
937            let resp = client.post(&path, &body).await.map_err(|e| e.to_string())?;
938            let goal = resp.get("goal").cloned().unwrap_or(resp);
939            Ok(compact_items(&[goal], &["id", "name"]))
940        }
941
942        "clickup_goal_update" => {
943            let goal_id = args
944                .get("goal_id")
945                .and_then(|v| v.as_str())
946                .ok_or("Missing required parameter: goal_id")?;
947            let mut body = json!({});
948            if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
949                body["name"] = json!(name);
950            }
951            if let Some(due_date) = args.get("due_date").and_then(|v| v.as_i64()) {
952                body["due_date"] = json!(due_date);
953            }
954            if let Some(desc) = args.get("description").and_then(|v| v.as_str()) {
955                body["description"] = json!(desc);
956            }
957            let path = format!("/v2/goal/{}", goal_id);
958            let resp = client.put(&path, &body).await.map_err(|e| e.to_string())?;
959            let goal = resp.get("goal").cloned().unwrap_or(resp);
960            Ok(compact_items(&[goal], &["id", "name"]))
961        }
962
963        "clickup_view_list" => {
964            let path = if let Some(space_id) = args.get("space_id").and_then(|v| v.as_str()) {
965                format!("/v2/space/{}/view", space_id)
966            } else if let Some(folder_id) = args.get("folder_id").and_then(|v| v.as_str()) {
967                format!("/v2/folder/{}/view", folder_id)
968            } else if let Some(list_id) = args.get("list_id").and_then(|v| v.as_str()) {
969                format!("/v2/list/{}/view", list_id)
970            } else {
971                let team_id = resolve_workspace(args)?;
972                format!("/v2/team/{}/view", team_id)
973            };
974            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
975            let views = resp.get("views").and_then(|v| v.as_array()).cloned().unwrap_or_default();
976            Ok(compact_items(&views, &["id", "name", "type"]))
977        }
978
979        "clickup_view_tasks" => {
980            let view_id = args
981                .get("view_id")
982                .and_then(|v| v.as_str())
983                .ok_or("Missing required parameter: view_id")?;
984            let page = args.get("page").and_then(|v| v.as_i64()).unwrap_or(0);
985            let path = format!("/v2/view/{}/task?page={}", view_id, page);
986            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
987            let tasks = resp.get("tasks").and_then(|t| t.as_array()).cloned().unwrap_or_default();
988            Ok(compact_items(&tasks, &["id", "name", "status", "priority", "assignees", "due_date"]))
989        }
990
991        "clickup_doc_list" => {
992            let team_id = resolve_workspace(args)?;
993            let path = format!("/v3/workspaces/{}/docs", team_id);
994            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
995            let docs = resp.get("docs").and_then(|d| d.as_array()).cloned().unwrap_or_default();
996            Ok(compact_items(&docs, &["id", "name", "date_created"]))
997        }
998
999        "clickup_doc_get" => {
1000            let team_id = resolve_workspace(args)?;
1001            let doc_id = args
1002                .get("doc_id")
1003                .and_then(|v| v.as_str())
1004                .ok_or("Missing required parameter: doc_id")?;
1005            let path = format!("/v3/workspaces/{}/docs/{}", team_id, doc_id);
1006            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1007            Ok(compact_items(&[resp], &["id", "name", "date_created"]))
1008        }
1009
1010        "clickup_doc_pages" => {
1011            let team_id = resolve_workspace(args)?;
1012            let doc_id = args
1013                .get("doc_id")
1014                .and_then(|v| v.as_str())
1015                .ok_or("Missing required parameter: doc_id")?;
1016            let content = args.get("content").and_then(|v| v.as_bool()).unwrap_or(false);
1017            let path = format!("/v3/workspaces/{}/docs/{}/pages?content_format=text/md&max_page_depth=-1&include_content={}", team_id, doc_id, content);
1018            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1019            let pages = resp.get("pages").and_then(|p| p.as_array()).cloned().unwrap_or_default();
1020            Ok(compact_items(&pages, &["id", "name"]))
1021        }
1022
1023        "clickup_tag_list" => {
1024            let space_id = args
1025                .get("space_id")
1026                .and_then(|v| v.as_str())
1027                .ok_or("Missing required parameter: space_id")?;
1028            let path = format!("/v2/space/{}/tag", space_id);
1029            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1030            let tags = resp.get("tags").and_then(|t| t.as_array()).cloned().unwrap_or_default();
1031            Ok(compact_items(&tags, &["name", "tag_fg", "tag_bg"]))
1032        }
1033
1034        "clickup_task_add_tag" => {
1035            let task_id = args
1036                .get("task_id")
1037                .and_then(|v| v.as_str())
1038                .ok_or("Missing required parameter: task_id")?;
1039            let tag_name = args
1040                .get("tag_name")
1041                .and_then(|v| v.as_str())
1042                .ok_or("Missing required parameter: tag_name")?;
1043            let path = format!("/v2/task/{}/tag/{}", task_id, tag_name);
1044            client.post(&path, &json!({})).await.map_err(|e| e.to_string())?;
1045            Ok(json!({"message": format!("Tag '{}' added to task {}", tag_name, task_id)}))
1046        }
1047
1048        "clickup_task_remove_tag" => {
1049            let task_id = args
1050                .get("task_id")
1051                .and_then(|v| v.as_str())
1052                .ok_or("Missing required parameter: task_id")?;
1053            let tag_name = args
1054                .get("tag_name")
1055                .and_then(|v| v.as_str())
1056                .ok_or("Missing required parameter: tag_name")?;
1057            let path = format!("/v2/task/{}/tag/{}", task_id, tag_name);
1058            client.delete(&path).await.map_err(|e| e.to_string())?;
1059            Ok(json!({"message": format!("Tag '{}' removed from task {}", tag_name, task_id)}))
1060        }
1061
1062        "clickup_webhook_list" => {
1063            let team_id = resolve_workspace(args)?;
1064            let path = format!("/v2/team/{}/webhook", team_id);
1065            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1066            let webhooks = resp.get("webhooks").and_then(|w| w.as_array()).cloned().unwrap_or_default();
1067            Ok(compact_items(&webhooks, &["id", "endpoint", "events", "status"]))
1068        }
1069
1070        "clickup_member_list" => {
1071            let path = if let Some(task_id) = args.get("task_id").and_then(|v| v.as_str()) {
1072                format!("/v2/task/{}/member", task_id)
1073            } else if let Some(list_id) = args.get("list_id").and_then(|v| v.as_str()) {
1074                format!("/v2/list/{}/member", list_id)
1075            } else {
1076                return Err("Provide either task_id or list_id".to_string());
1077            };
1078            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1079            let members = resp.get("members").and_then(|m| m.as_array()).cloned().unwrap_or_default();
1080            Ok(compact_items(&members, &["id", "username", "email"]))
1081        }
1082
1083        "clickup_template_list" => {
1084            let team_id = resolve_workspace(args)?;
1085            let page = args.get("page").and_then(|v| v.as_i64()).unwrap_or(0);
1086            let path = format!("/v2/team/{}/taskTemplate?page={}", team_id, page);
1087            let resp = client.get(&path).await.map_err(|e| e.to_string())?;
1088            let templates = resp.get("templates").and_then(|t| t.as_array()).cloned().unwrap_or_default();
1089            Ok(compact_items(&templates, &["id", "name"]))
1090        }
1091
1092        unknown => Err(format!("Unknown tool: {}", unknown)),
1093    }
1094}
1095
1096// ── Main server loop ──────────────────────────────────────────────────────────
1097
1098pub async fn serve() -> Result<(), Box<dyn std::error::Error>> {
1099    // Load config at startup
1100    let config = Config::load().map_err(|e| format!("Failed to load config: {}", e))?;
1101    let token = config.auth.token.clone();
1102    if token.is_empty() {
1103        return Err("No API token configured. Run `clickup setup` first.".into());
1104    }
1105    let workspace_id = config.defaults.workspace_id.clone();
1106
1107    let client = ClickUpClient::new(&token, 30)
1108        .map_err(|e| format!("Failed to create HTTP client: {}", e))?;
1109
1110    let stdin = tokio::io::stdin();
1111    let reader = BufReader::new(stdin);
1112    let mut lines = reader.lines();
1113
1114    while let Some(line) = lines.next_line().await? {
1115        let line = line.trim().to_string();
1116        if line.is_empty() {
1117            continue;
1118        }
1119
1120        let msg: Value = match serde_json::from_str(&line) {
1121            Ok(v) => v,
1122            Err(e) => {
1123                // Parse error — send error response with null id
1124                let resp = error_response(&Value::Null, -32700, &format!("Parse error: {}", e));
1125                println!("{}", resp);
1126                continue;
1127            }
1128        };
1129
1130        // Notifications have no id — don't respond
1131        let id = msg.get("id").cloned().unwrap_or(Value::Null);
1132        let method = msg.get("method").and_then(|v| v.as_str()).unwrap_or("");
1133
1134        if id.is_null() && method.starts_with("notifications/") {
1135            // Notification — no response needed
1136            continue;
1137        }
1138
1139        let resp = match method {
1140            "initialize" => {
1141                let version = msg
1142                    .get("params")
1143                    .and_then(|p| p.get("protocolVersion"))
1144                    .and_then(|v| v.as_str())
1145                    .unwrap_or("2024-11-05");
1146                ok_response(
1147                    &id,
1148                    json!({
1149                        "protocolVersion": version,
1150                        "capabilities": {"tools": {}},
1151                        "serverInfo": {
1152                            "name": "clickup-cli",
1153                            "version": env!("CARGO_PKG_VERSION")
1154                        }
1155                    }),
1156                )
1157            }
1158
1159            "tools/list" => ok_response(&id, json!({"tools": tool_list()})),
1160
1161            "tools/call" => {
1162                let params = msg.get("params").cloned().unwrap_or(json!({}));
1163                let tool_name = params
1164                    .get("name")
1165                    .and_then(|v| v.as_str())
1166                    .unwrap_or("");
1167                let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
1168
1169                if tool_name.is_empty() {
1170                    let result = tool_error("Missing tool name".to_string());
1171                    ok_response(&id, result)
1172                } else {
1173                    let result = call_tool(tool_name, &arguments, &client, &workspace_id).await;
1174                    ok_response(&id, result)
1175                }
1176            }
1177
1178            other => {
1179                // Unknown method
1180                eprintln!("Unknown method: {}", other);
1181                error_response(&id, -32601, &format!("Method not found: {}", other))
1182            }
1183        };
1184
1185        println!("{}", resp);
1186    }
1187
1188    Ok(())
1189}