1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use things3_core::{
7 BackupManager, DataExporter, PerformanceMonitor, ThingsCache, ThingsConfig, ThingsDatabase,
8};
9
10#[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
40pub 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 pub async fn list_tools(&self) -> Result<ListToolsResult> {
73 Ok(ListToolsResult {
74 tools: self.get_available_tools().await?,
75 })
76 }
77
78 pub async fn call_tool(&self, request: CallToolRequest) -> Result<CallToolResult> {
80 self.handle_tool_call(request).await
81 }
82
83 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 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 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 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 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 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}