things3_cli/
mcp.rs

1//! MCP (Model Context Protocol) server implementation for Things 3 integration
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use things3_core::{
7    BackupManager, DataExporter, PerformanceMonitor, ThingsCache, ThingsConfig, ThingsDatabase,
8};
9
10/// Simplified MCP types for our implementation
11#[derive(Debug, Serialize, Deserialize)]
12pub struct Tool {
13    pub name: String,
14    pub description: String,
15    pub input_schema: Value,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct CallToolRequest {
20    pub name: String,
21    pub arguments: Option<Value>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct CallToolResult {
26    pub content: Vec<Content>,
27    pub is_error: bool,
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31pub enum Content {
32    Text { text: String },
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct ListToolsResult {
37    pub tools: Vec<Tool>,
38}
39
40/// MCP server for Things 3 integration
41pub struct ThingsMcpServer {
42    #[allow(dead_code)]
43    db: ThingsDatabase,
44    #[allow(dead_code)]
45    cache: ThingsCache,
46    #[allow(dead_code)]
47    performance_monitor: PerformanceMonitor,
48    #[allow(dead_code)]
49    exporter: DataExporter,
50    #[allow(dead_code)]
51    backup_manager: BackupManager,
52}
53
54#[allow(dead_code)]
55impl ThingsMcpServer {
56    pub fn new(db: ThingsDatabase, config: ThingsConfig) -> Self {
57        let cache = ThingsCache::new_default();
58        let performance_monitor = PerformanceMonitor::new_default();
59        let exporter = DataExporter::new_default();
60        let backup_manager = BackupManager::new(config);
61
62        Self {
63            db,
64            cache,
65            performance_monitor,
66            exporter,
67            backup_manager,
68        }
69    }
70
71    /// List available MCP tools
72    pub async fn list_tools(&self) -> Result<ListToolsResult> {
73        Ok(ListToolsResult {
74            tools: self.get_available_tools().await?,
75        })
76    }
77
78    /// Call a specific MCP tool
79    pub async fn call_tool(&self, request: CallToolRequest) -> Result<CallToolResult> {
80        self.handle_tool_call(request).await
81    }
82
83    /// Get available MCP tools
84    async fn get_available_tools(&self) -> Result<Vec<Tool>> {
85        Ok(vec![
86            Tool {
87                name: "get_inbox".to_string(),
88                description: "Get tasks from the inbox".to_string(),
89                input_schema: serde_json::json!({
90                    "type": "object",
91                    "properties": {
92                        "limit": {
93                            "type": "integer",
94                            "description": "Maximum number of tasks to return"
95                        }
96                    }
97                }),
98            },
99            Tool {
100                name: "get_today".to_string(),
101                description: "Get tasks scheduled for today".to_string(),
102                input_schema: serde_json::json!({
103                    "type": "object",
104                    "properties": {
105                        "limit": {
106                            "type": "integer",
107                            "description": "Maximum number of tasks to return"
108                        }
109                    }
110                }),
111            },
112            Tool {
113                name: "get_projects".to_string(),
114                description: "Get all projects, optionally filtered by area".to_string(),
115                input_schema: serde_json::json!({
116                    "type": "object",
117                    "properties": {
118                        "area_uuid": {
119                            "type": "string",
120                            "description": "Optional area UUID to filter projects"
121                        }
122                    }
123                }),
124            },
125            Tool {
126                name: "get_areas".to_string(),
127                description: "Get all areas".to_string(),
128                input_schema: serde_json::json!({
129                    "type": "object",
130                    "properties": {}
131                }),
132            },
133            Tool {
134                name: "search_tasks".to_string(),
135                description: "Search for tasks by query".to_string(),
136                input_schema: serde_json::json!({
137                    "type": "object",
138                    "properties": {
139                        "query": {
140                            "type": "string",
141                            "description": "Search query"
142                        },
143                        "limit": {
144                            "type": "integer",
145                            "description": "Maximum number of tasks to return"
146                        }
147                    },
148                    "required": ["query"]
149                }),
150            },
151            Tool {
152                name: "create_task".to_string(),
153                description: "Create a new task".to_string(),
154                input_schema: serde_json::json!({
155                    "type": "object",
156                    "properties": {
157                        "title": {
158                            "type": "string",
159                            "description": "Task title"
160                        },
161                        "notes": {
162                            "type": "string",
163                            "description": "Optional task notes"
164                        },
165                        "project_uuid": {
166                            "type": "string",
167                            "description": "Optional project UUID"
168                        },
169                        "area_uuid": {
170                            "type": "string",
171                            "description": "Optional area UUID"
172                        }
173                    },
174                    "required": ["title"]
175                }),
176            },
177            Tool {
178                name: "update_task".to_string(),
179                description: "Update an existing task".to_string(),
180                input_schema: serde_json::json!({
181                    "type": "object",
182                    "properties": {
183                        "uuid": {
184                            "type": "string",
185                            "description": "Task UUID"
186                        },
187                        "title": {
188                            "type": "string",
189                            "description": "New task title"
190                        },
191                        "notes": {
192                            "type": "string",
193                            "description": "New task notes"
194                        },
195                        "status": {
196                            "type": "string",
197                            "description": "New task status",
198                            "enum": ["incomplete", "completed", "canceled", "trashed"]
199                        }
200                    },
201                    "required": ["uuid"]
202                }),
203            },
204            Tool {
205                name: "get_productivity_metrics".to_string(),
206                description: "Get productivity metrics and statistics".to_string(),
207                input_schema: serde_json::json!({
208                    "type": "object",
209                    "properties": {
210                        "days": {
211                            "type": "integer",
212                            "description": "Number of days to look back for metrics"
213                        }
214                    }
215                }),
216            },
217            Tool {
218                name: "export_data".to_string(),
219                description: "Export data in various formats".to_string(),
220                input_schema: serde_json::json!({
221                    "type": "object",
222                    "properties": {
223                        "format": {
224                            "type": "string",
225                            "description": "Export format",
226                            "enum": ["json", "csv", "markdown"]
227                        },
228                        "data_type": {
229                            "type": "string",
230                            "description": "Type of data to export",
231                            "enum": ["tasks", "projects", "areas", "all"]
232                        }
233                    },
234                    "required": ["format", "data_type"]
235                }),
236            },
237            Tool {
238                name: "bulk_create_tasks".to_string(),
239                description: "Create multiple tasks at once".to_string(),
240                input_schema: serde_json::json!({
241                    "type": "object",
242                    "properties": {
243                        "tasks": {
244                            "type": "array",
245                            "description": "Array of task objects to create",
246                            "items": {
247                                "type": "object",
248                                "properties": {
249                                    "title": {"type": "string"},
250                                    "notes": {"type": "string"},
251                                    "project_uuid": {"type": "string"},
252                                    "area_uuid": {"type": "string"}
253                                },
254                                "required": ["title"]
255                            }
256                        }
257                    },
258                    "required": ["tasks"]
259                }),
260            },
261            Tool {
262                name: "get_recent_tasks".to_string(),
263                description: "Get recently created or modified tasks".to_string(),
264                input_schema: serde_json::json!({
265                    "type": "object",
266                    "properties": {
267                        "limit": {
268                            "type": "integer",
269                            "description": "Maximum number of tasks to return"
270                        },
271                        "hours": {
272                            "type": "integer",
273                            "description": "Number of hours to look back"
274                        }
275                    }
276                }),
277            },
278            Tool {
279                name: "backup_database".to_string(),
280                description: "Create a backup of the Things 3 database".to_string(),
281                input_schema: serde_json::json!({
282                    "type": "object",
283                    "properties": {
284                        "backup_dir": {
285                            "type": "string",
286                            "description": "Directory to store the backup"
287                        },
288                        "description": {
289                            "type": "string",
290                            "description": "Optional description for the backup"
291                        }
292                    },
293                    "required": ["backup_dir"]
294                }),
295            },
296            Tool {
297                name: "restore_database".to_string(),
298                description: "Restore from a backup".to_string(),
299                input_schema: serde_json::json!({
300                    "type": "object",
301                    "properties": {
302                        "backup_path": {
303                            "type": "string",
304                            "description": "Path to the backup file"
305                        }
306                    },
307                    "required": ["backup_path"]
308                }),
309            },
310            Tool {
311                name: "list_backups".to_string(),
312                description: "List available backups".to_string(),
313                input_schema: serde_json::json!({
314                    "type": "object",
315                    "properties": {
316                        "backup_dir": {
317                            "type": "string",
318                            "description": "Directory containing backups"
319                        }
320                    },
321                    "required": ["backup_dir"]
322                }),
323            },
324            Tool {
325                name: "get_performance_stats".to_string(),
326                description: "Get performance statistics and metrics".to_string(),
327                input_schema: serde_json::json!({
328                    "type": "object",
329                    "properties": {}
330                }),
331            },
332            Tool {
333                name: "get_system_metrics".to_string(),
334                description: "Get current system resource metrics".to_string(),
335                input_schema: serde_json::json!({
336                    "type": "object",
337                    "properties": {}
338                }),
339            },
340            Tool {
341                name: "get_cache_stats".to_string(),
342                description: "Get cache statistics and hit rates".to_string(),
343                input_schema: serde_json::json!({
344                    "type": "object",
345                    "properties": {}
346                }),
347            },
348        ])
349    }
350
351    /// Handle tool call
352    async fn handle_tool_call(&self, request: CallToolRequest) -> Result<CallToolResult> {
353        let tool_name = &request.name;
354        let arguments = request.arguments.unwrap_or_default();
355
356        let result = match tool_name.as_str() {
357            "get_inbox" => self.handle_get_inbox(arguments).await,
358            "get_today" => self.handle_get_today(arguments).await,
359            "get_projects" => self.handle_get_projects(arguments).await,
360            "get_areas" => self.handle_get_areas(arguments).await,
361            "search_tasks" => self.handle_search_tasks(arguments).await,
362            "create_task" => self.handle_create_task(arguments).await,
363            "update_task" => self.handle_update_task(arguments).await,
364            "get_productivity_metrics" => self.handle_get_productivity_metrics(arguments).await,
365            "export_data" => self.handle_export_data(arguments).await,
366            "bulk_create_tasks" => self.handle_bulk_create_tasks(arguments).await,
367            "get_recent_tasks" => self.handle_get_recent_tasks(arguments).await,
368            "backup_database" => self.handle_backup_database(arguments).await,
369            "restore_database" => self.handle_restore_database(arguments).await,
370            "list_backups" => self.handle_list_backups(arguments).await,
371            "get_performance_stats" => self.handle_get_performance_stats(arguments).await,
372            "get_system_metrics" => self.handle_get_system_metrics(arguments).await,
373            "get_cache_stats" => self.handle_get_cache_stats(arguments).await,
374            _ => {
375                return Ok(CallToolResult {
376                    content: vec![Content::Text {
377                        text: format!("Unknown tool: {}", tool_name),
378                    }],
379                    is_error: true,
380                });
381            }
382        };
383
384        match result {
385            Ok(call_result) => Ok(call_result),
386            Err(e) => Ok(CallToolResult {
387                content: vec![Content::Text {
388                    text: format!("Error: {}", e),
389                }],
390                is_error: true,
391            }),
392        }
393    }
394
395    async fn handle_get_inbox(&self, args: Value) -> Result<CallToolResult> {
396        let limit = args
397            .get("limit")
398            .and_then(|v| v.as_u64())
399            .map(|v| v as usize);
400        let tasks = self.db.get_inbox(limit)?;
401        let json = serde_json::to_string_pretty(&tasks)?;
402        Ok(CallToolResult {
403            content: vec![Content::Text { text: json }],
404            is_error: false,
405        })
406    }
407
408    async fn handle_get_today(&self, args: Value) -> Result<CallToolResult> {
409        let limit = args
410            .get("limit")
411            .and_then(|v| v.as_u64())
412            .map(|v| v as usize);
413        let tasks = self.db.get_today(limit)?;
414        let json = serde_json::to_string_pretty(&tasks)?;
415        Ok(CallToolResult {
416            content: vec![Content::Text { text: json }],
417            is_error: false,
418        })
419    }
420
421    async fn handle_get_projects(&self, args: Value) -> Result<CallToolResult> {
422        let area_uuid = args
423            .get("area_uuid")
424            .and_then(|v| v.as_str())
425            .and_then(|s| uuid::Uuid::parse_str(s).ok());
426        let projects = self.db.get_projects(area_uuid)?;
427        let json = serde_json::to_string_pretty(&projects)?;
428        Ok(CallToolResult {
429            content: vec![Content::Text { text: json }],
430            is_error: false,
431        })
432    }
433
434    async fn handle_get_areas(&self, _args: Value) -> Result<CallToolResult> {
435        let areas = self.db.get_areas()?;
436        let json = serde_json::to_string_pretty(&areas)?;
437        Ok(CallToolResult {
438            content: vec![Content::Text { text: json }],
439            is_error: false,
440        })
441    }
442
443    async fn handle_search_tasks(&self, args: Value) -> Result<CallToolResult> {
444        let query = args
445            .get("query")
446            .and_then(|v| v.as_str())
447            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: query"))?;
448        let limit = args
449            .get("limit")
450            .and_then(|v| v.as_u64())
451            .map(|v| v as usize);
452        let tasks = self.db.search_tasks(query, limit)?;
453        let json = serde_json::to_string_pretty(&tasks)?;
454        Ok(CallToolResult {
455            content: vec![Content::Text { text: json }],
456            is_error: false,
457        })
458    }
459
460    async fn handle_create_task(&self, args: Value) -> Result<CallToolResult> {
461        // Note: This is a placeholder - actual task creation would need to be implemented
462        // in the things-core library
463        let title = args
464            .get("title")
465            .and_then(|v| v.as_str())
466            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: title"))?;
467
468        let response = serde_json::json!({
469            "message": "Task creation not yet implemented",
470            "title": title,
471            "status": "placeholder"
472        });
473
474        Ok(CallToolResult {
475            content: vec![Content::Text {
476                text: serde_json::to_string_pretty(&response)?,
477            }],
478            is_error: false,
479        })
480    }
481
482    async fn handle_update_task(&self, args: Value) -> Result<CallToolResult> {
483        // Note: This is a placeholder - actual task updating would need to be implemented
484        // in the things-core library
485        let uuid = args
486            .get("uuid")
487            .and_then(|v| v.as_str())
488            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: uuid"))?;
489
490        let response = serde_json::json!({
491            "message": "Task updating not yet implemented",
492            "uuid": uuid,
493            "status": "placeholder"
494        });
495
496        Ok(CallToolResult {
497            content: vec![Content::Text {
498                text: serde_json::to_string_pretty(&response)?,
499            }],
500            is_error: false,
501        })
502    }
503
504    async fn handle_get_productivity_metrics(&self, args: Value) -> Result<CallToolResult> {
505        let days = args.get("days").and_then(|v| v.as_u64()).unwrap_or(7) as usize;
506
507        // Get various metrics
508        let inbox_tasks = self.db.get_inbox(None)?;
509        let today_tasks = self.db.get_today(None)?;
510        let projects = self.db.get_projects(None)?;
511        let areas = self.db.get_areas()?;
512
513        let metrics = serde_json::json!({
514            "period_days": days,
515            "inbox_tasks_count": inbox_tasks.len(),
516            "today_tasks_count": today_tasks.len(),
517            "projects_count": projects.len(),
518            "areas_count": areas.len(),
519            "completed_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Completed).count(),
520            "incomplete_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Incomplete).count(),
521            "timestamp": chrono::Utc::now()
522        });
523
524        Ok(CallToolResult {
525            content: vec![Content::Text {
526                text: serde_json::to_string_pretty(&metrics)?,
527            }],
528            is_error: false,
529        })
530    }
531
532    async fn handle_export_data(&self, args: Value) -> Result<CallToolResult> {
533        let format = args
534            .get("format")
535            .and_then(|v| v.as_str())
536            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: format"))?;
537        let data_type = args
538            .get("data_type")
539            .and_then(|v| v.as_str())
540            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: data_type"))?;
541
542        let export_data = match data_type {
543            "tasks" => {
544                let inbox = self.db.get_inbox(None)?;
545                let today = self.db.get_today(None)?;
546                serde_json::json!({
547                    "inbox": inbox,
548                    "today": today
549                })
550            }
551            "projects" => {
552                let projects = self.db.get_projects(None)?;
553                serde_json::json!({ "projects": projects })
554            }
555            "areas" => {
556                let areas = self.db.get_areas()?;
557                serde_json::json!({ "areas": areas })
558            }
559            "all" => {
560                let inbox = self.db.get_inbox(None)?;
561                let today = self.db.get_today(None)?;
562                let projects = self.db.get_projects(None)?;
563                let areas = self.db.get_areas()?;
564                serde_json::json!({
565                    "inbox": inbox,
566                    "today": today,
567                    "projects": projects,
568                    "areas": areas
569                })
570            }
571            _ => return Err(anyhow::anyhow!("Invalid data_type: {}", data_type)),
572        };
573
574        let result = match format {
575            "json" => serde_json::to_string_pretty(&export_data)?,
576            "csv" => "CSV export not yet implemented".to_string(),
577            "markdown" => "Markdown export not yet implemented".to_string(),
578            _ => return Err(anyhow::anyhow!("Invalid format: {}", format)),
579        };
580
581        Ok(CallToolResult {
582            content: vec![Content::Text { text: result }],
583            is_error: false,
584        })
585    }
586
587    async fn handle_bulk_create_tasks(&self, args: Value) -> Result<CallToolResult> {
588        let tasks = args
589            .get("tasks")
590            .and_then(|v| v.as_array())
591            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: tasks"))?;
592
593        let response = serde_json::json!({
594            "message": "Bulk task creation not yet implemented",
595            "tasks_count": tasks.len(),
596            "status": "placeholder"
597        });
598
599        Ok(CallToolResult {
600            content: vec![Content::Text {
601                text: serde_json::to_string_pretty(&response)?,
602            }],
603            is_error: false,
604        })
605    }
606
607    async fn handle_get_recent_tasks(&self, args: Value) -> Result<CallToolResult> {
608        let limit = args
609            .get("limit")
610            .and_then(|v| v.as_u64())
611            .map(|v| v as usize);
612        let hours = args.get("hours").and_then(|v| v.as_u64()).unwrap_or(24) as i64;
613
614        // For now, return inbox tasks as a proxy for recent tasks
615        // In a real implementation, this would query by creation/modification date
616        let tasks = self.db.get_inbox(limit)?;
617
618        let response = serde_json::json!({
619            "message": "Recent tasks (using inbox as proxy)",
620            "hours_lookback": hours,
621            "tasks": tasks
622        });
623
624        Ok(CallToolResult {
625            content: vec![Content::Text {
626                text: serde_json::to_string_pretty(&response)?,
627            }],
628            is_error: false,
629        })
630    }
631
632    async fn handle_backup_database(&self, args: Value) -> Result<CallToolResult> {
633        let backup_dir = args
634            .get("backup_dir")
635            .and_then(|v| v.as_str())
636            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: backup_dir"))?;
637        let description = args.get("description").and_then(|v| v.as_str());
638
639        let backup_path = std::path::Path::new(backup_dir);
640        let metadata = self
641            .backup_manager
642            .create_backup(backup_path, description)
643            .await?;
644
645        let response = serde_json::json!({
646            "message": "Backup created successfully",
647            "backup_path": metadata.backup_path,
648            "file_size": metadata.file_size,
649            "created_at": metadata.created_at
650        });
651
652        Ok(CallToolResult {
653            content: vec![Content::Text {
654                text: serde_json::to_string_pretty(&response)?,
655            }],
656            is_error: false,
657        })
658    }
659
660    async fn handle_restore_database(&self, args: Value) -> Result<CallToolResult> {
661        let backup_path = args
662            .get("backup_path")
663            .and_then(|v| v.as_str())
664            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: backup_path"))?;
665
666        let backup_file = std::path::Path::new(backup_path);
667        self.backup_manager.restore_backup(backup_file).await?;
668
669        let response = serde_json::json!({
670            "message": "Database restored successfully",
671            "backup_path": backup_path
672        });
673
674        Ok(CallToolResult {
675            content: vec![Content::Text {
676                text: serde_json::to_string_pretty(&response)?,
677            }],
678            is_error: false,
679        })
680    }
681
682    async fn handle_list_backups(&self, args: Value) -> Result<CallToolResult> {
683        let backup_dir = args
684            .get("backup_dir")
685            .and_then(|v| v.as_str())
686            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: backup_dir"))?;
687
688        let backup_path = std::path::Path::new(backup_dir);
689        let backups = self.backup_manager.list_backups(backup_path)?;
690
691        let response = serde_json::json!({
692            "backups": backups,
693            "count": backups.len()
694        });
695
696        Ok(CallToolResult {
697            content: vec![Content::Text {
698                text: serde_json::to_string_pretty(&response)?,
699            }],
700            is_error: false,
701        })
702    }
703
704    async fn handle_get_performance_stats(&self, _args: Value) -> Result<CallToolResult> {
705        let stats = self.performance_monitor.get_all_stats();
706        let summary = self.performance_monitor.get_summary();
707
708        let response = serde_json::json!({
709            "summary": summary,
710            "operation_stats": stats
711        });
712
713        Ok(CallToolResult {
714            content: vec![Content::Text {
715                text: serde_json::to_string_pretty(&response)?,
716            }],
717            is_error: false,
718        })
719    }
720
721    async fn handle_get_system_metrics(&self, _args: Value) -> Result<CallToolResult> {
722        let metrics = self.performance_monitor.get_system_metrics()?;
723
724        Ok(CallToolResult {
725            content: vec![Content::Text {
726                text: serde_json::to_string_pretty(&metrics)?,
727            }],
728            is_error: false,
729        })
730    }
731
732    async fn handle_get_cache_stats(&self, _args: Value) -> Result<CallToolResult> {
733        let stats = self.cache.get_stats();
734
735        Ok(CallToolResult {
736            content: vec![Content::Text {
737                text: serde_json::to_string_pretty(&stats)?,
738            }],
739            is_error: false,
740        })
741    }
742}