1use crate::client::ClickUpClient;
2use crate::config::Config;
3use crate::output::compact_items;
4use serde_json::{json, Value};
5use tokio::io::{AsyncBufReadExt, BufReader};
6
7fn 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
25fn 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
512async 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 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
1096pub async fn serve() -> Result<(), Box<dyn std::error::Error>> {
1099 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 let resp = error_response(&Value::Null, -32700, &format!("Parse error: {}", e));
1125 println!("{}", resp);
1126 continue;
1127 }
1128 };
1129
1130 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 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 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}