1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::sync::Arc;
6use things3_core::{
7 BackupManager, DataExporter, DeleteChildHandling, McpServerConfig, PerformanceMonitor,
8 ThingsCache, ThingsConfig, ThingsDatabase, ThingsError,
9};
10use thiserror::Error;
11use tokio::sync::Mutex;
12use uuid::Uuid;
13
14pub mod io_wrapper;
15pub mod middleware;
16pub mod test_harness;
18
19use io_wrapper::{McpIo, StdIo};
20use middleware::{MiddlewareChain, MiddlewareConfig};
21
22#[derive(Error, Debug)]
24pub enum McpError {
25 #[error("Tool not found: {tool_name}")]
26 ToolNotFound { tool_name: String },
27
28 #[error("Resource not found: {uri}")]
29 ResourceNotFound { uri: String },
30
31 #[error("Prompt not found: {prompt_name}")]
32 PromptNotFound { prompt_name: String },
33
34 #[error("Invalid parameter: {parameter_name} - {message}")]
35 InvalidParameter {
36 parameter_name: String,
37 message: String,
38 },
39
40 #[error("Missing required parameter: {parameter_name}")]
41 MissingParameter { parameter_name: String },
42
43 #[error("Invalid format: {format} - supported formats: {supported}")]
44 InvalidFormat { format: String, supported: String },
45
46 #[error("Invalid data type: {data_type} - supported types: {supported}")]
47 InvalidDataType {
48 data_type: String,
49 supported: String,
50 },
51
52 #[error("Database operation failed: {operation}")]
53 DatabaseOperationFailed {
54 operation: String,
55 source: ThingsError,
56 },
57
58 #[error("Backup operation failed: {operation}")]
59 BackupOperationFailed {
60 operation: String,
61 source: ThingsError,
62 },
63
64 #[error("Export operation failed: {operation}")]
65 ExportOperationFailed {
66 operation: String,
67 source: ThingsError,
68 },
69
70 #[error("Performance monitoring failed: {operation}")]
71 PerformanceMonitoringFailed {
72 operation: String,
73 source: ThingsError,
74 },
75
76 #[error("Cache operation failed: {operation}")]
77 CacheOperationFailed {
78 operation: String,
79 source: ThingsError,
80 },
81
82 #[error("Serialization failed: {operation}")]
83 SerializationFailed {
84 operation: String,
85 source: serde_json::Error,
86 },
87
88 #[error("IO operation failed: {operation}")]
89 IoOperationFailed {
90 operation: String,
91 source: std::io::Error,
92 },
93
94 #[error("Configuration error: {message}")]
95 ConfigurationError { message: String },
96
97 #[error("Validation error: {message}")]
98 ValidationError { message: String },
99
100 #[error("Internal error: {message}")]
101 InternalError { message: String },
102}
103
104impl McpError {
105 pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
107 Self::ToolNotFound {
108 tool_name: tool_name.into(),
109 }
110 }
111
112 pub fn resource_not_found(uri: impl Into<String>) -> Self {
114 Self::ResourceNotFound { uri: uri.into() }
115 }
116
117 pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
119 Self::PromptNotFound {
120 prompt_name: prompt_name.into(),
121 }
122 }
123
124 pub fn invalid_parameter(
126 parameter_name: impl Into<String>,
127 message: impl Into<String>,
128 ) -> Self {
129 Self::InvalidParameter {
130 parameter_name: parameter_name.into(),
131 message: message.into(),
132 }
133 }
134
135 pub fn missing_parameter(parameter_name: impl Into<String>) -> Self {
137 Self::MissingParameter {
138 parameter_name: parameter_name.into(),
139 }
140 }
141
142 pub fn invalid_format(format: impl Into<String>, supported: impl Into<String>) -> Self {
144 Self::InvalidFormat {
145 format: format.into(),
146 supported: supported.into(),
147 }
148 }
149
150 pub fn invalid_data_type(data_type: impl Into<String>, supported: impl Into<String>) -> Self {
152 Self::InvalidDataType {
153 data_type: data_type.into(),
154 supported: supported.into(),
155 }
156 }
157
158 pub fn database_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
160 Self::DatabaseOperationFailed {
161 operation: operation.into(),
162 source,
163 }
164 }
165
166 pub fn backup_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
168 Self::BackupOperationFailed {
169 operation: operation.into(),
170 source,
171 }
172 }
173
174 pub fn export_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
176 Self::ExportOperationFailed {
177 operation: operation.into(),
178 source,
179 }
180 }
181
182 pub fn performance_monitoring_failed(
184 operation: impl Into<String>,
185 source: ThingsError,
186 ) -> Self {
187 Self::PerformanceMonitoringFailed {
188 operation: operation.into(),
189 source,
190 }
191 }
192
193 pub fn cache_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
195 Self::CacheOperationFailed {
196 operation: operation.into(),
197 source,
198 }
199 }
200
201 pub fn serialization_failed(operation: impl Into<String>, source: serde_json::Error) -> Self {
203 Self::SerializationFailed {
204 operation: operation.into(),
205 source,
206 }
207 }
208
209 pub fn io_operation_failed(operation: impl Into<String>, source: std::io::Error) -> Self {
211 Self::IoOperationFailed {
212 operation: operation.into(),
213 source,
214 }
215 }
216
217 pub fn configuration_error(message: impl Into<String>) -> Self {
219 Self::ConfigurationError {
220 message: message.into(),
221 }
222 }
223
224 pub fn validation_error(message: impl Into<String>) -> Self {
226 Self::ValidationError {
227 message: message.into(),
228 }
229 }
230
231 pub fn internal_error(message: impl Into<String>) -> Self {
233 Self::InternalError {
234 message: message.into(),
235 }
236 }
237
238 #[must_use]
240 pub fn to_call_result(self) -> CallToolResult {
241 let error_message = match &self {
242 McpError::ToolNotFound { tool_name } => {
243 format!("Tool '{tool_name}' not found. Available tools can be listed using the list_tools method.")
244 }
245 McpError::ResourceNotFound { uri } => {
246 format!("Resource '{uri}' not found. Available resources can be listed using the list_resources method.")
247 }
248 McpError::PromptNotFound { prompt_name } => {
249 format!("Prompt '{prompt_name}' not found. Available prompts can be listed using the list_prompts method.")
250 }
251 McpError::InvalidParameter {
252 parameter_name,
253 message,
254 } => {
255 format!("Invalid parameter '{parameter_name}': {message}. Please check the parameter format and try again.")
256 }
257 McpError::MissingParameter { parameter_name } => {
258 format!("Missing required parameter '{parameter_name}'. Please provide this parameter and try again.")
259 }
260 McpError::InvalidFormat { format, supported } => {
261 format!("Invalid format '{format}'. Supported formats: {supported}. Please use one of the supported formats.")
262 }
263 McpError::InvalidDataType {
264 data_type,
265 supported,
266 } => {
267 format!("Invalid data type '{data_type}'. Supported types: {supported}. Please use one of the supported types.")
268 }
269 McpError::DatabaseOperationFailed { operation, source } => {
270 format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
271 }
272 McpError::BackupOperationFailed { operation, source } => {
273 format!("Backup operation '{operation}' failed: {source}. Please check backup permissions and try again.")
274 }
275 McpError::ExportOperationFailed { operation, source } => {
276 format!("Export operation '{operation}' failed: {source}. Please check export parameters and try again.")
277 }
278 McpError::PerformanceMonitoringFailed { operation, source } => {
279 format!("Performance monitoring '{operation}' failed: {source}. Please try again later.")
280 }
281 McpError::CacheOperationFailed { operation, source } => {
282 format!("Cache operation '{operation}' failed: {source}. Please try again later.")
283 }
284 McpError::SerializationFailed { operation, source } => {
285 format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
286 }
287 McpError::IoOperationFailed { operation, source } => {
288 format!("IO operation '{operation}' failed: {source}. Please check file permissions and try again.")
289 }
290 McpError::ConfigurationError { message } => {
291 format!("Configuration error: {message}. Please check your configuration and try again.")
292 }
293 McpError::ValidationError { message } => {
294 format!("Validation error: {message}. Please check your input and try again.")
295 }
296 McpError::InternalError { message } => {
297 format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
298 }
299 };
300
301 CallToolResult {
302 content: vec![Content::Text {
303 text: error_message,
304 }],
305 is_error: true,
306 }
307 }
308
309 #[must_use]
311 pub fn to_prompt_result(self) -> GetPromptResult {
312 let error_message = match &self {
313 McpError::PromptNotFound { prompt_name } => {
314 format!("Prompt '{prompt_name}' not found. Available prompts can be listed using the list_prompts method.")
315 }
316 McpError::InvalidParameter {
317 parameter_name,
318 message,
319 } => {
320 format!("Invalid parameter '{parameter_name}': {message}. Please check the parameter format and try again.")
321 }
322 McpError::MissingParameter { parameter_name } => {
323 format!("Missing required parameter '{parameter_name}'. Please provide this parameter and try again.")
324 }
325 McpError::DatabaseOperationFailed { operation, source } => {
326 format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
327 }
328 McpError::SerializationFailed { operation, source } => {
329 format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
330 }
331 McpError::ValidationError { message } => {
332 format!("Validation error: {message}. Please check your input and try again.")
333 }
334 McpError::InternalError { message } => {
335 format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
336 }
337 _ => {
338 format!("Error: {self}. Please try again later.")
339 }
340 };
341
342 GetPromptResult {
343 content: vec![Content::Text {
344 text: error_message,
345 }],
346 is_error: true,
347 }
348 }
349
350 #[must_use]
352 pub fn to_resource_result(self) -> ReadResourceResult {
353 let error_message = match &self {
354 McpError::ResourceNotFound { uri } => {
355 format!("Resource '{uri}' not found. Available resources can be listed using the list_resources method.")
356 }
357 McpError::DatabaseOperationFailed { operation, source } => {
358 format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
359 }
360 McpError::SerializationFailed { operation, source } => {
361 format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
362 }
363 McpError::InternalError { message } => {
364 format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
365 }
366 _ => {
367 format!("Error: {self}. Please try again later.")
368 }
369 };
370
371 ReadResourceResult {
372 contents: vec![Content::Text {
373 text: error_message,
374 }],
375 }
376 }
377}
378
379pub type McpResult<T> = std::result::Result<T, McpError>;
381
382impl From<ThingsError> for McpError {
384 fn from(error: ThingsError) -> Self {
385 match error {
386 ThingsError::Database(e) => {
387 McpError::database_operation_failed("database operation", ThingsError::Database(e))
388 }
389 ThingsError::Serialization(e) => McpError::serialization_failed("serialization", e),
390 ThingsError::Io(e) => McpError::io_operation_failed("io operation", e),
391 ThingsError::DatabaseNotFound { path } => {
392 McpError::configuration_error(format!("Database not found at: {path}"))
393 }
394 ThingsError::InvalidUuid { uuid } => {
395 McpError::validation_error(format!("Invalid UUID format: {uuid}"))
396 }
397 ThingsError::InvalidDate { date } => {
398 McpError::validation_error(format!("Invalid date format: {date}"))
399 }
400 ThingsError::TaskNotFound { uuid } => {
401 McpError::validation_error(format!("Task not found: {uuid}"))
402 }
403 ThingsError::ProjectNotFound { uuid } => {
404 McpError::validation_error(format!("Project not found: {uuid}"))
405 }
406 ThingsError::AreaNotFound { uuid } => {
407 McpError::validation_error(format!("Area not found: {uuid}"))
408 }
409 ThingsError::Validation { message } => McpError::validation_error(message),
410 ThingsError::InvalidCursor(message) => {
411 McpError::validation_error(format!("Invalid cursor: {message}"))
412 }
413 ThingsError::Configuration { message } => McpError::configuration_error(message),
414 ThingsError::DateValidation(e) => {
415 McpError::validation_error(format!("Date validation failed: {e}"))
416 }
417 ThingsError::DateConversion(e) => {
418 McpError::validation_error(format!("Date conversion failed: {e}"))
419 }
420 ThingsError::Unknown { message } => McpError::internal_error(message),
421 }
422 }
423}
424
425impl From<serde_json::Error> for McpError {
426 fn from(error: serde_json::Error) -> Self {
427 McpError::serialization_failed("json serialization", error)
428 }
429}
430
431impl From<std::io::Error> for McpError {
432 fn from(error: std::io::Error) -> Self {
433 McpError::io_operation_failed("file operation", error)
434 }
435}
436
437#[derive(Debug, Serialize, Deserialize)]
439pub struct Tool {
440 pub name: String,
441 pub description: String,
442 pub input_schema: Value,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct CallToolRequest {
447 pub name: String,
448 pub arguments: Option<Value>,
449}
450
451#[derive(Debug, Serialize, Deserialize)]
452pub struct CallToolResult {
453 pub content: Vec<Content>,
454 pub is_error: bool,
455}
456
457#[derive(Debug, Serialize, Deserialize)]
458pub enum Content {
459 Text { text: String },
460}
461
462#[derive(Debug, Serialize, Deserialize)]
463pub struct ListToolsResult {
464 pub tools: Vec<Tool>,
465}
466
467#[derive(Debug, Serialize, Deserialize)]
469pub struct Resource {
470 pub uri: String,
471 pub name: String,
472 pub description: String,
473 pub mime_type: Option<String>,
474}
475
476#[derive(Debug, Serialize, Deserialize)]
477pub struct ListResourcesResult {
478 pub resources: Vec<Resource>,
479}
480
481#[derive(Debug, Serialize, Deserialize)]
482pub struct ReadResourceRequest {
483 pub uri: String,
484}
485
486#[derive(Debug, Serialize, Deserialize)]
487pub struct ReadResourceResult {
488 pub contents: Vec<Content>,
489}
490
491#[derive(Debug, Serialize, Deserialize)]
493pub struct Prompt {
494 pub name: String,
495 pub description: String,
496 pub arguments: Value,
497}
498
499#[derive(Debug, Serialize, Deserialize)]
500pub struct ListPromptsResult {
501 pub prompts: Vec<Prompt>,
502}
503
504#[derive(Debug, Serialize, Deserialize)]
505pub struct GetPromptRequest {
506 pub name: String,
507 pub arguments: Option<Value>,
508}
509
510#[derive(Debug, Serialize, Deserialize)]
511pub struct GetPromptResult {
512 pub content: Vec<Content>,
513 pub is_error: bool,
514}
515
516pub struct ThingsMcpServer {
518 #[allow(dead_code)]
519 pub db: Arc<ThingsDatabase>,
520 #[allow(dead_code)]
521 cache: Arc<Mutex<ThingsCache>>,
522 #[allow(dead_code)]
523 performance_monitor: Arc<Mutex<PerformanceMonitor>>,
524 #[allow(dead_code)]
525 exporter: DataExporter,
526 #[allow(dead_code)]
527 backup_manager: Arc<Mutex<BackupManager>>,
528 middleware_chain: MiddlewareChain,
530}
531
532#[allow(dead_code)]
533pub async fn start_mcp_server(
538 db: Arc<ThingsDatabase>,
539 config: ThingsConfig,
540) -> things3_core::Result<()> {
541 let io = StdIo::new();
542 start_mcp_server_generic(db, config, io).await
543}
544
545pub async fn start_mcp_server_generic<I: McpIo>(
550 db: Arc<ThingsDatabase>,
551 config: ThingsConfig,
552 mut io: I,
553) -> things3_core::Result<()> {
554 let server = Arc::new(tokio::sync::Mutex::new(ThingsMcpServer::new(db, config)));
555
556 loop {
558 let line = io.read_line().await.map_err(|e| {
560 things3_core::ThingsError::unknown(format!("Failed to read from input: {}", e))
561 })?;
562
563 let Some(line) = line else {
565 break;
566 };
567
568 if line.is_empty() {
570 continue;
571 }
572
573 let request: serde_json::Value = serde_json::from_str(&line).map_err(|e| {
575 things3_core::ThingsError::unknown(format!("Failed to parse JSON-RPC request: {}", e))
576 })?;
577
578 let server_clone = Arc::clone(&server);
580 let response_opt = {
581 let server = server_clone.lock().await;
582 server.handle_jsonrpc_request(request).await
583 }?;
584
585 if let Some(response) = response_opt {
587 let response_str = serde_json::to_string(&response).map_err(|e| {
588 things3_core::ThingsError::unknown(format!("Failed to serialize response: {}", e))
589 })?;
590
591 io.write_line(&response_str).await.map_err(|e| {
592 things3_core::ThingsError::unknown(format!("Failed to write response: {}", e))
593 })?;
594
595 io.flush().await.map_err(|e| {
596 things3_core::ThingsError::unknown(format!("Failed to flush output: {}", e))
597 })?;
598 }
599 }
601
602 Ok(())
603}
604
605pub async fn start_mcp_server_with_config(
614 db: Arc<ThingsDatabase>,
615 mcp_config: McpServerConfig,
616) -> things3_core::Result<()> {
617 let io = StdIo::new();
618 start_mcp_server_with_config_generic(db, mcp_config, io).await
619}
620
621pub async fn start_mcp_server_with_config_generic<I: McpIo>(
623 db: Arc<ThingsDatabase>,
624 mcp_config: McpServerConfig,
625 mut io: I,
626) -> things3_core::Result<()> {
627 let things_config = ThingsConfig::new(
629 mcp_config.database.path.clone(),
630 mcp_config.database.fallback_to_default,
631 );
632
633 let server = Arc::new(tokio::sync::Mutex::new(
634 ThingsMcpServer::new_with_mcp_config(db, things_config, mcp_config),
635 ));
636
637 loop {
639 let line = io.read_line().await.map_err(|e| {
641 things3_core::ThingsError::unknown(format!("Failed to read from input: {}", e))
642 })?;
643
644 let Some(line) = line else {
646 break;
647 };
648
649 if line.is_empty() {
651 continue;
652 }
653
654 let request: serde_json::Value = serde_json::from_str(&line).map_err(|e| {
656 things3_core::ThingsError::unknown(format!("Failed to parse JSON-RPC request: {}", e))
657 })?;
658
659 let server_clone = Arc::clone(&server);
661 let response_opt = {
662 let server = server_clone.lock().await;
663 server.handle_jsonrpc_request(request).await
664 }?;
665
666 if let Some(response) = response_opt {
668 let response_str = serde_json::to_string(&response).map_err(|e| {
669 things3_core::ThingsError::unknown(format!("Failed to serialize response: {}", e))
670 })?;
671
672 io.write_line(&response_str).await.map_err(|e| {
673 things3_core::ThingsError::unknown(format!("Failed to write response: {}", e))
674 })?;
675
676 io.flush().await.map_err(|e| {
677 things3_core::ThingsError::unknown(format!("Failed to flush output: {}", e))
678 })?;
679 }
680 }
682
683 Ok(())
684}
685
686impl ThingsMcpServer {
687 #[must_use]
688 pub fn new(db: Arc<ThingsDatabase>, config: ThingsConfig) -> Self {
689 let cache = ThingsCache::new_default();
690 let performance_monitor = PerformanceMonitor::new_default();
691 let exporter = DataExporter::new_default();
692 let backup_manager = BackupManager::new(config);
693 let mut middleware_config = MiddlewareConfig::default();
695 middleware_config.logging.enabled = false; let middleware_chain = middleware_config.build_chain();
697
698 Self {
699 db,
700 cache: Arc::new(Mutex::new(cache)),
701 performance_monitor: Arc::new(Mutex::new(performance_monitor)),
702 exporter,
703 backup_manager: Arc::new(Mutex::new(backup_manager)),
704 middleware_chain,
705 }
706 }
707
708 #[must_use]
710 pub fn with_middleware_config(
711 db: ThingsDatabase,
712 config: ThingsConfig,
713 middleware_config: MiddlewareConfig,
714 ) -> Self {
715 let cache = ThingsCache::new_default();
716 let performance_monitor = PerformanceMonitor::new_default();
717 let exporter = DataExporter::new_default();
718 let backup_manager = BackupManager::new(config);
719 let middleware_chain = middleware_config.build_chain();
720
721 Self {
722 db: Arc::new(db),
723 cache: Arc::new(Mutex::new(cache)),
724 performance_monitor: Arc::new(Mutex::new(performance_monitor)),
725 exporter,
726 backup_manager: Arc::new(Mutex::new(backup_manager)),
727 middleware_chain,
728 }
729 }
730
731 #[must_use]
733 pub fn new_with_mcp_config(
734 db: Arc<ThingsDatabase>,
735 config: ThingsConfig,
736 mcp_config: McpServerConfig,
737 ) -> Self {
738 let cache = ThingsCache::new_default();
739 let performance_monitor = PerformanceMonitor::new_default();
740 let exporter = DataExporter::new_default();
741 let backup_manager = BackupManager::new(config);
742
743 let middleware_config = MiddlewareConfig {
746 logging: middleware::LoggingConfig {
747 enabled: false, level: mcp_config.logging.level.clone(),
749 },
750 validation: middleware::ValidationConfig {
751 enabled: mcp_config.security.validation.enabled,
752 strict_mode: mcp_config.security.validation.strict_mode,
753 },
754 performance: middleware::PerformanceConfig {
755 enabled: mcp_config.performance.enabled,
756 slow_request_threshold_ms: mcp_config.performance.slow_request_threshold_ms,
757 },
758 security: middleware::SecurityConfig {
759 authentication: middleware::AuthenticationConfig {
760 enabled: mcp_config.security.authentication.enabled,
761 require_auth: mcp_config.security.authentication.require_auth,
762 jwt_secret: mcp_config.security.authentication.jwt_secret,
763 api_keys: mcp_config
764 .security
765 .authentication
766 .api_keys
767 .iter()
768 .map(|key| middleware::ApiKeyConfig {
769 key: key.key.clone(),
770 key_id: key.key_id.clone(),
771 permissions: key.permissions.clone(),
772 expires_at: key.expires_at.clone(),
773 })
774 .collect(),
775 oauth: mcp_config
776 .security
777 .authentication
778 .oauth
779 .as_ref()
780 .map(|oauth| middleware::OAuth2Config {
781 client_id: oauth.client_id.clone(),
782 client_secret: oauth.client_secret.clone(),
783 token_endpoint: oauth.token_endpoint.clone(),
784 scopes: oauth.scopes.clone(),
785 }),
786 },
787 rate_limiting: middleware::RateLimitingConfig {
788 enabled: mcp_config.security.rate_limiting.enabled,
789 requests_per_minute: mcp_config.security.rate_limiting.requests_per_minute,
790 burst_limit: mcp_config.security.rate_limiting.burst_limit,
791 custom_limits: mcp_config.security.rate_limiting.custom_limits.clone(),
792 },
793 },
794 };
795
796 let middleware_chain = middleware_config.build_chain();
797
798 Self {
799 db,
800 cache: Arc::new(Mutex::new(cache)),
801 performance_monitor: Arc::new(Mutex::new(performance_monitor)),
802 exporter,
803 backup_manager: Arc::new(Mutex::new(backup_manager)),
804 middleware_chain,
805 }
806 }
807
808 #[must_use]
810 pub fn middleware_chain(&self) -> &MiddlewareChain {
811 &self.middleware_chain
812 }
813
814 pub fn list_tools(&self) -> McpResult<ListToolsResult> {
819 Ok(ListToolsResult {
820 tools: Self::get_available_tools(),
821 })
822 }
823
824 pub async fn call_tool(&self, request: CallToolRequest) -> McpResult<CallToolResult> {
829 self.middleware_chain
830 .execute(
831 request,
832 |req| async move { self.handle_tool_call(req).await },
833 )
834 .await
835 }
836
837 pub async fn call_tool_with_fallback(&self, request: CallToolRequest) -> CallToolResult {
842 match self.handle_tool_call(request).await {
843 Ok(result) => result,
844 Err(error) => error.to_call_result(),
845 }
846 }
847
848 pub fn list_resources(&self) -> McpResult<ListResourcesResult> {
853 Ok(ListResourcesResult {
854 resources: Self::get_available_resources(),
855 })
856 }
857
858 pub async fn read_resource(
863 &self,
864 request: ReadResourceRequest,
865 ) -> McpResult<ReadResourceResult> {
866 self.handle_resource_read(request).await
867 }
868
869 pub async fn read_resource_with_fallback(
874 &self,
875 request: ReadResourceRequest,
876 ) -> ReadResourceResult {
877 match self.handle_resource_read(request).await {
878 Ok(result) => result,
879 Err(error) => error.to_resource_result(),
880 }
881 }
882
883 pub fn list_prompts(&self) -> McpResult<ListPromptsResult> {
888 Ok(ListPromptsResult {
889 prompts: Self::get_available_prompts(),
890 })
891 }
892
893 pub async fn get_prompt(&self, request: GetPromptRequest) -> McpResult<GetPromptResult> {
898 self.handle_prompt_request(request).await
899 }
900
901 pub async fn get_prompt_with_fallback(&self, request: GetPromptRequest) -> GetPromptResult {
906 match self.handle_prompt_request(request).await {
907 Ok(result) => result,
908 Err(error) => error.to_prompt_result(),
909 }
910 }
911
912 fn get_available_tools() -> Vec<Tool> {
914 let mut tools = Vec::new();
915 tools.extend(Self::get_data_retrieval_tools());
916 tools.extend(Self::get_task_management_tools());
917 tools.extend(Self::get_bulk_operation_tools());
918 tools.extend(Self::get_tag_management_tools());
919 tools.extend(Self::get_analytics_tools());
920 tools.extend(Self::get_backup_tools());
921 tools.extend(Self::get_system_tools());
922 tools
923 }
924
925 fn get_data_retrieval_tools() -> Vec<Tool> {
926 vec![
927 Tool {
928 name: "get_inbox".to_string(),
929 description: "Get tasks from the inbox".to_string(),
930 input_schema: serde_json::json!({
931 "type": "object",
932 "properties": {
933 "limit": {
934 "type": "integer",
935 "description": "Maximum number of tasks to return"
936 }
937 }
938 }),
939 },
940 Tool {
941 name: "get_today".to_string(),
942 description: "Get tasks scheduled for today".to_string(),
943 input_schema: serde_json::json!({
944 "type": "object",
945 "properties": {
946 "limit": {
947 "type": "integer",
948 "description": "Maximum number of tasks to return"
949 }
950 }
951 }),
952 },
953 Tool {
954 name: "get_projects".to_string(),
955 description: "Get all projects, optionally filtered by area".to_string(),
956 input_schema: serde_json::json!({
957 "type": "object",
958 "properties": {
959 "area_uuid": {
960 "type": "string",
961 "description": "Optional area UUID to filter projects"
962 }
963 }
964 }),
965 },
966 Tool {
967 name: "get_areas".to_string(),
968 description: "Get all areas".to_string(),
969 input_schema: serde_json::json!({
970 "type": "object",
971 "properties": {}
972 }),
973 },
974 Tool {
975 name: "search_tasks".to_string(),
976 description: "Search for tasks by query".to_string(),
977 input_schema: serde_json::json!({
978 "type": "object",
979 "properties": {
980 "query": {
981 "type": "string",
982 "description": "Search query"
983 },
984 "limit": {
985 "type": "integer",
986 "description": "Maximum number of tasks to return"
987 }
988 },
989 "required": ["query"]
990 }),
991 },
992 Tool {
993 name: "get_recent_tasks".to_string(),
994 description: "Get recently created or modified tasks".to_string(),
995 input_schema: serde_json::json!({
996 "type": "object",
997 "properties": {
998 "limit": {
999 "type": "integer",
1000 "description": "Maximum number of tasks to return"
1001 },
1002 "hours": {
1003 "type": "integer",
1004 "description": "Number of hours to look back"
1005 }
1006 }
1007 }),
1008 },
1009 Tool {
1010 name: "logbook_search".to_string(),
1011 description: "Search completed tasks in the Things 3 logbook. Supports text search, date ranges, and filtering by project/area/tags.".to_string(),
1012 input_schema: serde_json::json!({
1013 "type": "object",
1014 "properties": {
1015 "search_text": {
1016 "type": "string",
1017 "description": "Search in task titles and notes (case-insensitive)"
1018 },
1019 "from_date": {
1020 "type": "string",
1021 "format": "date",
1022 "description": "Start date for completion date range (YYYY-MM-DD)"
1023 },
1024 "to_date": {
1025 "type": "string",
1026 "format": "date",
1027 "description": "End date for completion date range (YYYY-MM-DD)"
1028 },
1029 "project_uuid": {
1030 "type": "string",
1031 "format": "uuid",
1032 "description": "Filter by project UUID"
1033 },
1034 "area_uuid": {
1035 "type": "string",
1036 "format": "uuid",
1037 "description": "Filter by area UUID"
1038 },
1039 "tags": {
1040 "type": "array",
1041 "items": { "type": "string" },
1042 "description": "Filter by one or more tags (all must match)"
1043 },
1044 "limit": {
1045 "type": "integer",
1046 "default": 50,
1047 "minimum": 1,
1048 "maximum": 500,
1049 "description": "Maximum number of results to return (default: 50, max: 500)"
1050 }
1051 }
1052 }),
1053 },
1054 ]
1055 }
1056
1057 fn get_task_management_tools() -> Vec<Tool> {
1058 vec![
1059 Tool {
1060 name: "create_task".to_string(),
1061 description: "Create a new task in Things 3".to_string(),
1062 input_schema: serde_json::json!({
1063 "type": "object",
1064 "properties": {
1065 "title": {
1066 "type": "string",
1067 "description": "Task title (required)"
1068 },
1069 "task_type": {
1070 "type": "string",
1071 "enum": ["to-do", "project", "heading"],
1072 "description": "Task type (default: to-do)"
1073 },
1074 "notes": {
1075 "type": "string",
1076 "description": "Task notes"
1077 },
1078 "start_date": {
1079 "type": "string",
1080 "format": "date",
1081 "description": "Start date (YYYY-MM-DD)"
1082 },
1083 "deadline": {
1084 "type": "string",
1085 "format": "date",
1086 "description": "Deadline (YYYY-MM-DD)"
1087 },
1088 "project_uuid": {
1089 "type": "string",
1090 "format": "uuid",
1091 "description": "Project UUID"
1092 },
1093 "area_uuid": {
1094 "type": "string",
1095 "format": "uuid",
1096 "description": "Area UUID"
1097 },
1098 "parent_uuid": {
1099 "type": "string",
1100 "format": "uuid",
1101 "description": "Parent task UUID (for subtasks)"
1102 },
1103 "tags": {
1104 "type": "array",
1105 "items": {"type": "string"},
1106 "description": "Tag names"
1107 },
1108 "status": {
1109 "type": "string",
1110 "enum": ["incomplete", "completed", "canceled", "trashed"],
1111 "description": "Initial status (default: incomplete)"
1112 }
1113 },
1114 "required": ["title"]
1115 }),
1116 },
1117 Tool {
1118 name: "update_task".to_string(),
1119 description: "Update an existing task (only provided fields will be updated)"
1120 .to_string(),
1121 input_schema: serde_json::json!({
1122 "type": "object",
1123 "properties": {
1124 "uuid": {
1125 "type": "string",
1126 "format": "uuid",
1127 "description": "Task UUID (required)"
1128 },
1129 "title": {
1130 "type": "string",
1131 "description": "New task title"
1132 },
1133 "notes": {
1134 "type": "string",
1135 "description": "New task notes"
1136 },
1137 "start_date": {
1138 "type": "string",
1139 "format": "date",
1140 "description": "New start date (YYYY-MM-DD)"
1141 },
1142 "deadline": {
1143 "type": "string",
1144 "format": "date",
1145 "description": "New deadline (YYYY-MM-DD)"
1146 },
1147 "status": {
1148 "type": "string",
1149 "enum": ["incomplete", "completed", "canceled", "trashed"],
1150 "description": "New task status"
1151 },
1152 "project_uuid": {
1153 "type": "string",
1154 "format": "uuid",
1155 "description": "New project UUID"
1156 },
1157 "area_uuid": {
1158 "type": "string",
1159 "format": "uuid",
1160 "description": "New area UUID"
1161 },
1162 "tags": {
1163 "type": "array",
1164 "items": {"type": "string"},
1165 "description": "New tag names"
1166 }
1167 },
1168 "required": ["uuid"]
1169 }),
1170 },
1171 Tool {
1172 name: "complete_task".to_string(),
1173 description: "Mark a task as completed".to_string(),
1174 input_schema: serde_json::json!({
1175 "type": "object",
1176 "properties": {
1177 "uuid": {
1178 "type": "string",
1179 "format": "uuid",
1180 "description": "UUID of the task to complete"
1181 }
1182 },
1183 "required": ["uuid"]
1184 }),
1185 },
1186 Tool {
1187 name: "uncomplete_task".to_string(),
1188 description: "Mark a completed task as incomplete".to_string(),
1189 input_schema: serde_json::json!({
1190 "type": "object",
1191 "properties": {
1192 "uuid": {
1193 "type": "string",
1194 "format": "uuid",
1195 "description": "UUID of the task to mark incomplete"
1196 }
1197 },
1198 "required": ["uuid"]
1199 }),
1200 },
1201 Tool {
1202 name: "delete_task".to_string(),
1203 description: "Soft delete a task (set trashed=1)".to_string(),
1204 input_schema: serde_json::json!({
1205 "type": "object",
1206 "properties": {
1207 "uuid": {
1208 "type": "string",
1209 "format": "uuid",
1210 "description": "UUID of the task to delete"
1211 },
1212 "child_handling": {
1213 "type": "string",
1214 "enum": ["error", "cascade", "orphan"],
1215 "default": "error",
1216 "description": "How to handle child tasks: error (fail if children exist), cascade (delete children too), orphan (delete parent only)"
1217 }
1218 },
1219 "required": ["uuid"]
1220 }),
1221 },
1222 Tool {
1223 name: "bulk_create_tasks".to_string(),
1224 description: "Create multiple tasks at once".to_string(),
1225 input_schema: serde_json::json!({
1226 "type": "object",
1227 "properties": {
1228 "tasks": {
1229 "type": "array",
1230 "description": "Array of task objects to create",
1231 "items": {
1232 "type": "object",
1233 "properties": {
1234 "title": {"type": "string"},
1235 "notes": {"type": "string"},
1236 "project_uuid": {"type": "string"},
1237 "area_uuid": {"type": "string"}
1238 },
1239 "required": ["title"]
1240 }
1241 }
1242 },
1243 "required": ["tasks"]
1244 }),
1245 },
1246 Tool {
1247 name: "create_project".to_string(),
1248 description: "Create a new project (a task with type=project)".to_string(),
1249 input_schema: serde_json::json!({
1250 "type": "object",
1251 "properties": {
1252 "title": {
1253 "type": "string",
1254 "description": "Project title (required)"
1255 },
1256 "notes": {
1257 "type": "string",
1258 "description": "Project notes"
1259 },
1260 "area_uuid": {
1261 "type": "string",
1262 "format": "uuid",
1263 "description": "Area UUID"
1264 },
1265 "start_date": {
1266 "type": "string",
1267 "format": "date",
1268 "description": "Start date (YYYY-MM-DD)"
1269 },
1270 "deadline": {
1271 "type": "string",
1272 "format": "date",
1273 "description": "Deadline (YYYY-MM-DD)"
1274 },
1275 "tags": {
1276 "type": "array",
1277 "items": {"type": "string"},
1278 "description": "Tag names"
1279 }
1280 },
1281 "required": ["title"]
1282 }),
1283 },
1284 Tool {
1285 name: "update_project".to_string(),
1286 description: "Update an existing project (only provided fields will be updated)".to_string(),
1287 input_schema: serde_json::json!({
1288 "type": "object",
1289 "properties": {
1290 "uuid": {
1291 "type": "string",
1292 "format": "uuid",
1293 "description": "Project UUID (required)"
1294 },
1295 "title": {
1296 "type": "string",
1297 "description": "New project title"
1298 },
1299 "notes": {
1300 "type": "string",
1301 "description": "New project notes"
1302 },
1303 "area_uuid": {
1304 "type": "string",
1305 "format": "uuid",
1306 "description": "New area UUID"
1307 },
1308 "start_date": {
1309 "type": "string",
1310 "format": "date",
1311 "description": "New start date (YYYY-MM-DD)"
1312 },
1313 "deadline": {
1314 "type": "string",
1315 "format": "date",
1316 "description": "New deadline (YYYY-MM-DD)"
1317 },
1318 "tags": {
1319 "type": "array",
1320 "items": {"type": "string"},
1321 "description": "New tag names"
1322 }
1323 },
1324 "required": ["uuid"]
1325 }),
1326 },
1327 Tool {
1328 name: "complete_project".to_string(),
1329 description: "Mark a project as completed, with options for handling child tasks".to_string(),
1330 input_schema: serde_json::json!({
1331 "type": "object",
1332 "properties": {
1333 "uuid": {
1334 "type": "string",
1335 "format": "uuid",
1336 "description": "UUID of the project to complete"
1337 },
1338 "child_handling": {
1339 "type": "string",
1340 "enum": ["error", "cascade", "orphan"],
1341 "default": "error",
1342 "description": "How to handle child tasks: error (fail if children exist), cascade (complete children too), orphan (move children to inbox)"
1343 }
1344 },
1345 "required": ["uuid"]
1346 }),
1347 },
1348 Tool {
1349 name: "delete_project".to_string(),
1350 description: "Soft delete a project (set trashed=1), with options for handling child tasks".to_string(),
1351 input_schema: serde_json::json!({
1352 "type": "object",
1353 "properties": {
1354 "uuid": {
1355 "type": "string",
1356 "format": "uuid",
1357 "description": "UUID of the project to delete"
1358 },
1359 "child_handling": {
1360 "type": "string",
1361 "enum": ["error", "cascade", "orphan"],
1362 "default": "error",
1363 "description": "How to handle child tasks: error (fail if children exist), cascade (delete children too), orphan (move children to inbox)"
1364 }
1365 },
1366 "required": ["uuid"]
1367 }),
1368 },
1369 Tool {
1370 name: "create_area".to_string(),
1371 description: "Create a new area".to_string(),
1372 input_schema: serde_json::json!({
1373 "type": "object",
1374 "properties": {
1375 "title": {
1376 "type": "string",
1377 "description": "Area title (required)"
1378 }
1379 },
1380 "required": ["title"]
1381 }),
1382 },
1383 Tool {
1384 name: "update_area".to_string(),
1385 description: "Update an existing area".to_string(),
1386 input_schema: serde_json::json!({
1387 "type": "object",
1388 "properties": {
1389 "uuid": {
1390 "type": "string",
1391 "format": "uuid",
1392 "description": "Area UUID (required)"
1393 },
1394 "title": {
1395 "type": "string",
1396 "description": "New area title (required)"
1397 }
1398 },
1399 "required": ["uuid", "title"]
1400 }),
1401 },
1402 Tool {
1403 name: "delete_area".to_string(),
1404 description: "Delete an area (hard delete). All projects in this area will be moved to no area.".to_string(),
1405 input_schema: serde_json::json!({
1406 "type": "object",
1407 "properties": {
1408 "uuid": {
1409 "type": "string",
1410 "format": "uuid",
1411 "description": "UUID of the area to delete"
1412 }
1413 },
1414 "required": ["uuid"]
1415 }),
1416 },
1417 ]
1418 }
1419
1420 fn get_analytics_tools() -> Vec<Tool> {
1421 vec![
1422 Tool {
1423 name: "get_productivity_metrics".to_string(),
1424 description: "Get productivity metrics and statistics".to_string(),
1425 input_schema: serde_json::json!({
1426 "type": "object",
1427 "properties": {
1428 "days": {
1429 "type": "integer",
1430 "description": "Number of days to look back for metrics"
1431 }
1432 }
1433 }),
1434 },
1435 Tool {
1436 name: "export_data".to_string(),
1437 description: "Export data in various formats".to_string(),
1438 input_schema: serde_json::json!({
1439 "type": "object",
1440 "properties": {
1441 "format": {
1442 "type": "string",
1443 "description": "Export format",
1444 "enum": ["json", "csv", "markdown"]
1445 },
1446 "data_type": {
1447 "type": "string",
1448 "description": "Type of data to export",
1449 "enum": ["tasks", "projects", "areas", "all"]
1450 }
1451 },
1452 "required": ["format", "data_type"]
1453 }),
1454 },
1455 ]
1456 }
1457
1458 fn get_backup_tools() -> Vec<Tool> {
1459 vec![
1460 Tool {
1461 name: "backup_database".to_string(),
1462 description: "Create a backup of the Things 3 database".to_string(),
1463 input_schema: serde_json::json!({
1464 "type": "object",
1465 "properties": {
1466 "backup_dir": {
1467 "type": "string",
1468 "description": "Directory to store the backup"
1469 },
1470 "description": {
1471 "type": "string",
1472 "description": "Optional description for the backup"
1473 }
1474 },
1475 "required": ["backup_dir"]
1476 }),
1477 },
1478 Tool {
1479 name: "restore_database".to_string(),
1480 description: "Restore from a backup".to_string(),
1481 input_schema: serde_json::json!({
1482 "type": "object",
1483 "properties": {
1484 "backup_path": {
1485 "type": "string",
1486 "description": "Path to the backup file"
1487 }
1488 },
1489 "required": ["backup_path"]
1490 }),
1491 },
1492 Tool {
1493 name: "list_backups".to_string(),
1494 description: "List available backups".to_string(),
1495 input_schema: serde_json::json!({
1496 "type": "object",
1497 "properties": {
1498 "backup_dir": {
1499 "type": "string",
1500 "description": "Directory containing backups"
1501 }
1502 },
1503 "required": ["backup_dir"]
1504 }),
1505 },
1506 ]
1507 }
1508
1509 fn get_system_tools() -> Vec<Tool> {
1510 vec![
1511 Tool {
1512 name: "get_performance_stats".to_string(),
1513 description: "Get performance statistics and metrics".to_string(),
1514 input_schema: serde_json::json!({
1515 "type": "object",
1516 "properties": {}
1517 }),
1518 },
1519 Tool {
1520 name: "get_system_metrics".to_string(),
1521 description: "Get current system resource metrics".to_string(),
1522 input_schema: serde_json::json!({
1523 "type": "object",
1524 "properties": {}
1525 }),
1526 },
1527 Tool {
1528 name: "get_cache_stats".to_string(),
1529 description: "Get cache statistics and hit rates".to_string(),
1530 input_schema: serde_json::json!({
1531 "type": "object",
1532 "properties": {}
1533 }),
1534 },
1535 ]
1536 }
1537
1538 fn get_bulk_operation_tools() -> Vec<Tool> {
1539 vec![
1540 Tool {
1541 name: "bulk_move".to_string(),
1542 description: "Move multiple tasks to a project or area (transactional)".to_string(),
1543 input_schema: serde_json::json!({
1544 "type": "object",
1545 "properties": {
1546 "task_uuids": {
1547 "type": "array",
1548 "items": {"type": "string"},
1549 "description": "Array of task UUIDs to move"
1550 },
1551 "project_uuid": {
1552 "type": "string",
1553 "format": "uuid",
1554 "description": "Target project UUID (optional)"
1555 },
1556 "area_uuid": {
1557 "type": "string",
1558 "format": "uuid",
1559 "description": "Target area UUID (optional)"
1560 }
1561 },
1562 "required": ["task_uuids"]
1563 }),
1564 },
1565 Tool {
1566 name: "bulk_update_dates".to_string(),
1567 description: "Update dates for multiple tasks with validation (transactional)"
1568 .to_string(),
1569 input_schema: serde_json::json!({
1570 "type": "object",
1571 "properties": {
1572 "task_uuids": {
1573 "type": "array",
1574 "items": {"type": "string"},
1575 "description": "Array of task UUIDs to update"
1576 },
1577 "start_date": {
1578 "type": "string",
1579 "format": "date",
1580 "description": "New start date (YYYY-MM-DD, optional)"
1581 },
1582 "deadline": {
1583 "type": "string",
1584 "format": "date",
1585 "description": "New deadline (YYYY-MM-DD, optional)"
1586 },
1587 "clear_start_date": {
1588 "type": "boolean",
1589 "description": "Clear start date (set to NULL, default: false)"
1590 },
1591 "clear_deadline": {
1592 "type": "boolean",
1593 "description": "Clear deadline (set to NULL, default: false)"
1594 }
1595 },
1596 "required": ["task_uuids"]
1597 }),
1598 },
1599 Tool {
1600 name: "bulk_complete".to_string(),
1601 description: "Mark multiple tasks as completed (transactional)".to_string(),
1602 input_schema: serde_json::json!({
1603 "type": "object",
1604 "properties": {
1605 "task_uuids": {
1606 "type": "array",
1607 "items": {"type": "string"},
1608 "description": "Array of task UUIDs to complete"
1609 }
1610 },
1611 "required": ["task_uuids"]
1612 }),
1613 },
1614 Tool {
1615 name: "bulk_delete".to_string(),
1616 description: "Delete multiple tasks (soft delete, transactional)".to_string(),
1617 input_schema: serde_json::json!({
1618 "type": "object",
1619 "properties": {
1620 "task_uuids": {
1621 "type": "array",
1622 "items": {"type": "string"},
1623 "description": "Array of task UUIDs to delete"
1624 }
1625 },
1626 "required": ["task_uuids"]
1627 }),
1628 },
1629 ]
1630 }
1631
1632 fn get_tag_management_tools() -> Vec<Tool> {
1633 vec![
1634 Tool {
1636 name: "search_tags".to_string(),
1637 description: "Search for existing tags (finds exact and similar matches)"
1638 .to_string(),
1639 input_schema: serde_json::json!({
1640 "type": "object",
1641 "properties": {
1642 "query": {
1643 "type": "string",
1644 "description": "Search query for tag titles"
1645 },
1646 "include_similar": {
1647 "type": "boolean",
1648 "description": "Include fuzzy matches (default: true)"
1649 },
1650 "min_similarity": {
1651 "type": "number",
1652 "description": "Minimum similarity score 0.0-1.0 (default: 0.7)"
1653 }
1654 },
1655 "required": ["query"]
1656 }),
1657 },
1658 Tool {
1659 name: "get_tag_suggestions".to_string(),
1660 description: "Get tag suggestions for a title (prevents duplicates)".to_string(),
1661 input_schema: serde_json::json!({
1662 "type": "object",
1663 "properties": {
1664 "title": {
1665 "type": "string",
1666 "description": "Proposed tag title"
1667 }
1668 },
1669 "required": ["title"]
1670 }),
1671 },
1672 Tool {
1673 name: "get_popular_tags".to_string(),
1674 description: "Get most frequently used tags".to_string(),
1675 input_schema: serde_json::json!({
1676 "type": "object",
1677 "properties": {
1678 "limit": {
1679 "type": "integer",
1680 "description": "Maximum number of tags to return (default: 20)"
1681 }
1682 }
1683 }),
1684 },
1685 Tool {
1686 name: "get_recent_tags".to_string(),
1687 description: "Get recently used tags".to_string(),
1688 input_schema: serde_json::json!({
1689 "type": "object",
1690 "properties": {
1691 "limit": {
1692 "type": "integer",
1693 "description": "Maximum number of tags to return (default: 20)"
1694 }
1695 }
1696 }),
1697 },
1698 Tool {
1700 name: "create_tag".to_string(),
1701 description: "Create a new tag (checks for duplicates first)".to_string(),
1702 input_schema: serde_json::json!({
1703 "type": "object",
1704 "properties": {
1705 "title": {
1706 "type": "string",
1707 "description": "Tag title (required)"
1708 },
1709 "shortcut": {
1710 "type": "string",
1711 "description": "Keyboard shortcut"
1712 },
1713 "parent_uuid": {
1714 "type": "string",
1715 "format": "uuid",
1716 "description": "Parent tag UUID for nesting"
1717 },
1718 "force": {
1719 "type": "boolean",
1720 "description": "Skip duplicate check (default: false)"
1721 }
1722 },
1723 "required": ["title"]
1724 }),
1725 },
1726 Tool {
1727 name: "update_tag".to_string(),
1728 description: "Update an existing tag".to_string(),
1729 input_schema: serde_json::json!({
1730 "type": "object",
1731 "properties": {
1732 "uuid": {
1733 "type": "string",
1734 "format": "uuid",
1735 "description": "Tag UUID (required)"
1736 },
1737 "title": {
1738 "type": "string",
1739 "description": "New title"
1740 },
1741 "shortcut": {
1742 "type": "string",
1743 "description": "New shortcut"
1744 },
1745 "parent_uuid": {
1746 "type": "string",
1747 "format": "uuid",
1748 "description": "New parent UUID"
1749 }
1750 },
1751 "required": ["uuid"]
1752 }),
1753 },
1754 Tool {
1755 name: "delete_tag".to_string(),
1756 description: "Delete a tag".to_string(),
1757 input_schema: serde_json::json!({
1758 "type": "object",
1759 "properties": {
1760 "uuid": {
1761 "type": "string",
1762 "format": "uuid",
1763 "description": "Tag UUID (required)"
1764 },
1765 "remove_from_tasks": {
1766 "type": "boolean",
1767 "description": "Remove tag from all tasks (default: false)"
1768 }
1769 },
1770 "required": ["uuid"]
1771 }),
1772 },
1773 Tool {
1774 name: "merge_tags".to_string(),
1775 description: "Merge two tags (combine source into target)".to_string(),
1776 input_schema: serde_json::json!({
1777 "type": "object",
1778 "properties": {
1779 "source_uuid": {
1780 "type": "string",
1781 "format": "uuid",
1782 "description": "UUID of tag to merge from (will be deleted)"
1783 },
1784 "target_uuid": {
1785 "type": "string",
1786 "format": "uuid",
1787 "description": "UUID of tag to merge into (will remain)"
1788 }
1789 },
1790 "required": ["source_uuid", "target_uuid"]
1791 }),
1792 },
1793 Tool {
1795 name: "add_tag_to_task".to_string(),
1796 description: "Add a tag to a task (suggests existing tags)".to_string(),
1797 input_schema: serde_json::json!({
1798 "type": "object",
1799 "properties": {
1800 "task_uuid": {
1801 "type": "string",
1802 "format": "uuid",
1803 "description": "Task UUID (required)"
1804 },
1805 "tag_title": {
1806 "type": "string",
1807 "description": "Tag title (required)"
1808 }
1809 },
1810 "required": ["task_uuid", "tag_title"]
1811 }),
1812 },
1813 Tool {
1814 name: "remove_tag_from_task".to_string(),
1815 description: "Remove a tag from a task".to_string(),
1816 input_schema: serde_json::json!({
1817 "type": "object",
1818 "properties": {
1819 "task_uuid": {
1820 "type": "string",
1821 "format": "uuid",
1822 "description": "Task UUID (required)"
1823 },
1824 "tag_title": {
1825 "type": "string",
1826 "description": "Tag title (required)"
1827 }
1828 },
1829 "required": ["task_uuid", "tag_title"]
1830 }),
1831 },
1832 Tool {
1833 name: "set_task_tags".to_string(),
1834 description: "Replace all tags on a task".to_string(),
1835 input_schema: serde_json::json!({
1836 "type": "object",
1837 "properties": {
1838 "task_uuid": {
1839 "type": "string",
1840 "format": "uuid",
1841 "description": "Task UUID (required)"
1842 },
1843 "tag_titles": {
1844 "type": "array",
1845 "items": {"type": "string"},
1846 "description": "Array of tag titles"
1847 }
1848 },
1849 "required": ["task_uuid", "tag_titles"]
1850 }),
1851 },
1852 Tool {
1854 name: "get_tag_statistics".to_string(),
1855 description: "Get detailed statistics for a tag".to_string(),
1856 input_schema: serde_json::json!({
1857 "type": "object",
1858 "properties": {
1859 "uuid": {
1860 "type": "string",
1861 "format": "uuid",
1862 "description": "Tag UUID (required)"
1863 }
1864 },
1865 "required": ["uuid"]
1866 }),
1867 },
1868 Tool {
1869 name: "find_duplicate_tags".to_string(),
1870 description: "Find duplicate or highly similar tags".to_string(),
1871 input_schema: serde_json::json!({
1872 "type": "object",
1873 "properties": {
1874 "min_similarity": {
1875 "type": "number",
1876 "description": "Minimum similarity score 0.0-1.0 (default: 0.85)"
1877 }
1878 }
1879 }),
1880 },
1881 Tool {
1882 name: "get_tag_completions".to_string(),
1883 description: "Get tag auto-completions for partial input".to_string(),
1884 input_schema: serde_json::json!({
1885 "type": "object",
1886 "properties": {
1887 "partial_input": {
1888 "type": "string",
1889 "description": "Partial tag input (required)"
1890 },
1891 "limit": {
1892 "type": "integer",
1893 "description": "Maximum completions to return (default: 10)"
1894 }
1895 },
1896 "required": ["partial_input"]
1897 }),
1898 },
1899 ]
1900 }
1901
1902 async fn handle_tool_call(&self, request: CallToolRequest) -> McpResult<CallToolResult> {
1904 let tool_name = &request.name;
1905 let arguments = request.arguments.unwrap_or_default();
1906
1907 let result = match tool_name.as_str() {
1908 "get_inbox" => self.handle_get_inbox(arguments).await,
1909 "get_today" => self.handle_get_today(arguments).await,
1910 "get_projects" => self.handle_get_projects(arguments).await,
1911 "get_areas" => self.handle_get_areas(arguments).await,
1912 "search_tasks" => self.handle_search_tasks(arguments).await,
1913 "logbook_search" => self.handle_logbook_search(arguments).await,
1914 "create_task" => self.handle_create_task(arguments).await,
1915 "update_task" => self.handle_update_task(arguments).await,
1916 "complete_task" => self.handle_complete_task(arguments).await,
1917 "uncomplete_task" => self.handle_uncomplete_task(arguments).await,
1918 "delete_task" => self.handle_delete_task(arguments).await,
1919 "bulk_move" => self.handle_bulk_move(arguments).await,
1920 "bulk_update_dates" => self.handle_bulk_update_dates(arguments).await,
1921 "bulk_complete" => self.handle_bulk_complete(arguments).await,
1922 "bulk_delete" => self.handle_bulk_delete(arguments).await,
1923 "create_project" => self.handle_create_project(arguments).await,
1924 "update_project" => self.handle_update_project(arguments).await,
1925 "complete_project" => self.handle_complete_project(arguments).await,
1926 "delete_project" => self.handle_delete_project(arguments).await,
1927 "create_area" => self.handle_create_area(arguments).await,
1928 "update_area" => self.handle_update_area(arguments).await,
1929 "delete_area" => self.handle_delete_area(arguments).await,
1930 "get_productivity_metrics" => self.handle_get_productivity_metrics(arguments).await,
1931 "export_data" => self.handle_export_data(arguments).await,
1932 "bulk_create_tasks" => Self::handle_bulk_create_tasks(&arguments),
1933 "get_recent_tasks" => self.handle_get_recent_tasks(arguments).await,
1934 "backup_database" => self.handle_backup_database(arguments).await,
1935 "restore_database" => self.handle_restore_database(arguments).await,
1936 "list_backups" => self.handle_list_backups(arguments).await,
1937 "get_performance_stats" => self.handle_get_performance_stats(arguments).await,
1938 "get_system_metrics" => self.handle_get_system_metrics(arguments).await,
1939 "get_cache_stats" => self.handle_get_cache_stats(arguments).await,
1940 "search_tags" => self.handle_search_tags_tool(arguments).await,
1942 "get_tag_suggestions" => self.handle_get_tag_suggestions(arguments).await,
1943 "get_popular_tags" => self.handle_get_popular_tags(arguments).await,
1944 "get_recent_tags" => self.handle_get_recent_tags(arguments).await,
1945 "create_tag" => self.handle_create_tag(arguments).await,
1947 "update_tag" => self.handle_update_tag(arguments).await,
1948 "delete_tag" => self.handle_delete_tag(arguments).await,
1949 "merge_tags" => self.handle_merge_tags(arguments).await,
1950 "add_tag_to_task" => self.handle_add_tag_to_task(arguments).await,
1952 "remove_tag_from_task" => self.handle_remove_tag_from_task(arguments).await,
1953 "set_task_tags" => self.handle_set_task_tags(arguments).await,
1954 "get_tag_statistics" => self.handle_get_tag_statistics(arguments).await,
1956 "find_duplicate_tags" => self.handle_find_duplicate_tags(arguments).await,
1957 "get_tag_completions" => self.handle_get_tag_completions(arguments).await,
1958 _ => {
1959 return Err(McpError::tool_not_found(tool_name));
1960 }
1961 };
1962
1963 result
1964 }
1965
1966 async fn handle_get_inbox(&self, args: Value) -> McpResult<CallToolResult> {
1967 let limit = args
1968 .get("limit")
1969 .and_then(serde_json::Value::as_u64)
1970 .map(|v| usize::try_from(v).unwrap_or(usize::MAX));
1971
1972 let tasks = self
1973 .db
1974 .get_inbox(limit)
1975 .await
1976 .map_err(|e| McpError::database_operation_failed("get_inbox", e))?;
1977
1978 let json = serde_json::to_string_pretty(&tasks)
1979 .map_err(|e| McpError::serialization_failed("get_inbox serialization", e))?;
1980
1981 Ok(CallToolResult {
1982 content: vec![Content::Text { text: json }],
1983 is_error: false,
1984 })
1985 }
1986
1987 async fn handle_get_today(&self, args: Value) -> McpResult<CallToolResult> {
1988 let limit = args
1989 .get("limit")
1990 .and_then(serde_json::Value::as_u64)
1991 .map(|v| usize::try_from(v).unwrap_or(usize::MAX));
1992
1993 let tasks = self.db.get_today(limit).await.map_err(|e| {
1994 McpError::database_operation_failed(
1996 "get_today",
1997 things3_core::ThingsError::unknown(format!("Failed to get today's tasks: {}", e)),
1998 )
1999 })?;
2000
2001 let json = serde_json::to_string_pretty(&tasks)
2002 .map_err(|e| McpError::serialization_failed("get_today serialization", e))?;
2003
2004 Ok(CallToolResult {
2005 content: vec![Content::Text { text: json }],
2006 is_error: false,
2007 })
2008 }
2009
2010 async fn handle_get_projects(&self, args: Value) -> McpResult<CallToolResult> {
2011 let _area_uuid = args
2012 .get("area_uuid")
2013 .and_then(|v| v.as_str())
2014 .and_then(|s| uuid::Uuid::parse_str(s).ok());
2015
2016 let projects = self
2017 .db
2018 .get_projects(None)
2019 .await
2020 .map_err(|e| McpError::database_operation_failed("get_projects", e))?;
2021
2022 let json = serde_json::to_string_pretty(&projects)
2023 .map_err(|e| McpError::serialization_failed("get_projects serialization", e))?;
2024
2025 Ok(CallToolResult {
2026 content: vec![Content::Text { text: json }],
2027 is_error: false,
2028 })
2029 }
2030
2031 async fn handle_get_areas(&self, _args: Value) -> McpResult<CallToolResult> {
2032 let areas = self
2033 .db
2034 .get_areas()
2035 .await
2036 .map_err(|e| McpError::database_operation_failed("get_areas", e))?;
2037
2038 let json = serde_json::to_string_pretty(&areas)
2039 .map_err(|e| McpError::serialization_failed("get_areas serialization", e))?;
2040
2041 Ok(CallToolResult {
2042 content: vec![Content::Text { text: json }],
2043 is_error: false,
2044 })
2045 }
2046
2047 async fn handle_search_tasks(&self, args: Value) -> McpResult<CallToolResult> {
2048 let query = args
2049 .get("query")
2050 .and_then(|v| v.as_str())
2051 .ok_or_else(|| McpError::missing_parameter("query"))?;
2052
2053 let _limit = args
2054 .get("limit")
2055 .and_then(serde_json::Value::as_u64)
2056 .map(|v| usize::try_from(v).unwrap_or(usize::MAX));
2057
2058 let tasks = self
2059 .db
2060 .search_tasks(query)
2061 .await
2062 .map_err(|e| McpError::database_operation_failed("search_tasks", e))?;
2063
2064 let json = serde_json::to_string_pretty(&tasks)
2065 .map_err(|e| McpError::serialization_failed("search_tasks serialization", e))?;
2066
2067 Ok(CallToolResult {
2068 content: vec![Content::Text { text: json }],
2069 is_error: false,
2070 })
2071 }
2072
2073 async fn handle_logbook_search(&self, args: Value) -> McpResult<CallToolResult> {
2074 let search_text = args
2076 .get("search_text")
2077 .and_then(|v| v.as_str())
2078 .map(|s| s.to_string());
2079
2080 let from_date = args
2081 .get("from_date")
2082 .and_then(|v| v.as_str())
2083 .and_then(|s| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
2084
2085 let to_date = args
2086 .get("to_date")
2087 .and_then(|v| v.as_str())
2088 .and_then(|s| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
2089
2090 let project_uuid = args
2091 .get("project_uuid")
2092 .and_then(|v| v.as_str())
2093 .and_then(|s| Uuid::parse_str(s).ok());
2094
2095 let area_uuid = args
2096 .get("area_uuid")
2097 .and_then(|v| v.as_str())
2098 .and_then(|s| Uuid::parse_str(s).ok());
2099
2100 let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
2101 arr.iter()
2102 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2103 .collect::<Vec<String>>()
2104 });
2105
2106 let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as u32);
2107
2108 let tasks = self
2110 .db
2111 .search_logbook(
2112 search_text,
2113 from_date,
2114 to_date,
2115 project_uuid,
2116 area_uuid,
2117 tags,
2118 limit,
2119 )
2120 .await
2121 .map_err(|e| McpError::database_operation_failed("logbook_search", e))?;
2122
2123 let json = serde_json::to_string_pretty(&tasks)
2125 .map_err(|e| McpError::serialization_failed("logbook_search serialization", e))?;
2126
2127 Ok(CallToolResult {
2128 content: vec![Content::Text { text: json }],
2129 is_error: false,
2130 })
2131 }
2132
2133 async fn handle_create_task(&self, args: Value) -> McpResult<CallToolResult> {
2134 let request: things3_core::CreateTaskRequest =
2136 serde_json::from_value(args).map_err(|e| {
2137 McpError::invalid_parameter(
2138 "request",
2139 format!("Failed to parse create task request: {e}"),
2140 )
2141 })?;
2142
2143 let uuid = self
2145 .db
2146 .create_task(request)
2147 .await
2148 .map_err(|e| McpError::database_operation_failed("create_task", e))?;
2149
2150 let response = serde_json::json!({
2152 "uuid": uuid,
2153 "message": "Task created successfully"
2154 });
2155
2156 Ok(CallToolResult {
2157 content: vec![Content::Text {
2158 text: serde_json::to_string_pretty(&response)
2159 .map_err(|e| McpError::serialization_failed("create_task response", e))?,
2160 }],
2161 is_error: false,
2162 })
2163 }
2164
2165 async fn handle_update_task(&self, args: Value) -> McpResult<CallToolResult> {
2166 let request: things3_core::UpdateTaskRequest =
2168 serde_json::from_value(args).map_err(|e| {
2169 McpError::invalid_parameter(
2170 "request",
2171 format!("Failed to parse update task request: {e}"),
2172 )
2173 })?;
2174
2175 self.db
2177 .update_task(request)
2178 .await
2179 .map_err(|e| McpError::database_operation_failed("update_task", e))?;
2180
2181 let response = serde_json::json!({
2183 "message": "Task updated successfully"
2184 });
2185
2186 Ok(CallToolResult {
2187 content: vec![Content::Text {
2188 text: serde_json::to_string_pretty(&response)
2189 .map_err(|e| McpError::serialization_failed("update_task response", e))?,
2190 }],
2191 is_error: false,
2192 })
2193 }
2194
2195 async fn handle_complete_task(&self, args: Value) -> McpResult<CallToolResult> {
2196 let uuid_str = args
2197 .get("uuid")
2198 .and_then(|v| v.as_str())
2199 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2200
2201 let uuid = uuid::Uuid::parse_str(uuid_str)
2202 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2203
2204 self.db
2205 .complete_task(&uuid)
2206 .await
2207 .map_err(|e| McpError::database_operation_failed("complete_task", e))?;
2208
2209 let response = serde_json::json!({
2210 "message": "Task completed successfully",
2211 "uuid": uuid_str
2212 });
2213
2214 Ok(CallToolResult {
2215 content: vec![Content::Text {
2216 text: serde_json::to_string_pretty(&response)
2217 .map_err(|e| McpError::serialization_failed("complete_task response", e))?,
2218 }],
2219 is_error: false,
2220 })
2221 }
2222
2223 async fn handle_uncomplete_task(&self, args: Value) -> McpResult<CallToolResult> {
2224 let uuid_str = args
2225 .get("uuid")
2226 .and_then(|v| v.as_str())
2227 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2228
2229 let uuid = uuid::Uuid::parse_str(uuid_str)
2230 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2231
2232 self.db
2233 .uncomplete_task(&uuid)
2234 .await
2235 .map_err(|e| McpError::database_operation_failed("uncomplete_task", e))?;
2236
2237 let response = serde_json::json!({
2238 "message": "Task marked as incomplete successfully",
2239 "uuid": uuid_str
2240 });
2241
2242 Ok(CallToolResult {
2243 content: vec![Content::Text {
2244 text: serde_json::to_string_pretty(&response)
2245 .map_err(|e| McpError::serialization_failed("uncomplete_task response", e))?,
2246 }],
2247 is_error: false,
2248 })
2249 }
2250
2251 async fn handle_delete_task(&self, args: Value) -> McpResult<CallToolResult> {
2252 let uuid_str = args
2253 .get("uuid")
2254 .and_then(|v| v.as_str())
2255 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2256
2257 let uuid = uuid::Uuid::parse_str(uuid_str)
2258 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2259
2260 let child_handling_str = args
2261 .get("child_handling")
2262 .and_then(|v| v.as_str())
2263 .unwrap_or("error");
2264
2265 let child_handling = match child_handling_str {
2266 "cascade" => DeleteChildHandling::Cascade,
2267 "orphan" => DeleteChildHandling::Orphan,
2268 _ => DeleteChildHandling::Error,
2269 };
2270
2271 self.db
2272 .delete_task(&uuid, child_handling)
2273 .await
2274 .map_err(|e| McpError::database_operation_failed("delete_task", e))?;
2275
2276 let response = serde_json::json!({
2277 "message": "Task deleted successfully",
2278 "uuid": uuid_str
2279 });
2280
2281 Ok(CallToolResult {
2282 content: vec![Content::Text {
2283 text: serde_json::to_string_pretty(&response)
2284 .map_err(|e| McpError::serialization_failed("delete_task response", e))?,
2285 }],
2286 is_error: false,
2287 })
2288 }
2289
2290 async fn handle_bulk_move(&self, args: Value) -> McpResult<CallToolResult> {
2295 let task_uuid_strs: Vec<String> = args
2297 .get("task_uuids")
2298 .and_then(|v| v.as_array())
2299 .ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
2300 .iter()
2301 .filter_map(|v| v.as_str().map(String::from))
2302 .collect();
2303
2304 let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
2305 .iter()
2306 .map(|s| {
2307 uuid::Uuid::parse_str(s).map_err(|e| {
2308 McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
2309 })
2310 })
2311 .collect::<McpResult<Vec<_>>>()?;
2312
2313 let project_uuid = args
2315 .get("project_uuid")
2316 .and_then(|v| v.as_str())
2317 .map(|s| {
2318 uuid::Uuid::parse_str(s).map_err(|e| {
2319 McpError::invalid_parameter("project_uuid", format!("Invalid UUID: {e}"))
2320 })
2321 })
2322 .transpose()?;
2323
2324 let area_uuid = args
2326 .get("area_uuid")
2327 .and_then(|v| v.as_str())
2328 .map(|s| {
2329 uuid::Uuid::parse_str(s).map_err(|e| {
2330 McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
2331 })
2332 })
2333 .transpose()?;
2334
2335 let request = things3_core::models::BulkMoveRequest {
2336 task_uuids,
2337 project_uuid,
2338 area_uuid,
2339 };
2340
2341 let result = self
2342 .db
2343 .bulk_move(request)
2344 .await
2345 .map_err(|e| McpError::database_operation_failed("bulk_move", e))?;
2346
2347 let response = serde_json::json!({
2348 "success": result.success,
2349 "processed_count": result.processed_count,
2350 "message": result.message
2351 });
2352
2353 Ok(CallToolResult {
2354 content: vec![Content::Text {
2355 text: serde_json::to_string_pretty(&response)
2356 .map_err(|e| McpError::serialization_failed("bulk_move response", e))?,
2357 }],
2358 is_error: false,
2359 })
2360 }
2361
2362 async fn handle_bulk_update_dates(&self, args: Value) -> McpResult<CallToolResult> {
2363 use chrono::NaiveDate;
2364
2365 let task_uuid_strs: Vec<String> = args
2367 .get("task_uuids")
2368 .and_then(|v| v.as_array())
2369 .ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
2370 .iter()
2371 .filter_map(|v| v.as_str().map(String::from))
2372 .collect();
2373
2374 let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
2375 .iter()
2376 .map(|s| {
2377 uuid::Uuid::parse_str(s).map_err(|e| {
2378 McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
2379 })
2380 })
2381 .collect::<McpResult<Vec<_>>>()?;
2382
2383 let start_date = args
2385 .get("start_date")
2386 .and_then(|v| v.as_str())
2387 .map(|s| {
2388 NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2389 McpError::invalid_parameter("start_date", format!("Invalid date format: {e}"))
2390 })
2391 })
2392 .transpose()?;
2393
2394 let deadline = args
2395 .get("deadline")
2396 .and_then(|v| v.as_str())
2397 .map(|s| {
2398 NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2399 McpError::invalid_parameter("deadline", format!("Invalid date format: {e}"))
2400 })
2401 })
2402 .transpose()?;
2403
2404 let clear_start_date = args
2405 .get("clear_start_date")
2406 .and_then(|v| v.as_bool())
2407 .unwrap_or(false);
2408
2409 let clear_deadline = args
2410 .get("clear_deadline")
2411 .and_then(|v| v.as_bool())
2412 .unwrap_or(false);
2413
2414 let request = things3_core::models::BulkUpdateDatesRequest {
2415 task_uuids,
2416 start_date,
2417 deadline,
2418 clear_start_date,
2419 clear_deadline,
2420 };
2421
2422 let result = self
2423 .db
2424 .bulk_update_dates(request)
2425 .await
2426 .map_err(|e| McpError::database_operation_failed("bulk_update_dates", e))?;
2427
2428 let response = serde_json::json!({
2429 "success": result.success,
2430 "processed_count": result.processed_count,
2431 "message": result.message
2432 });
2433
2434 Ok(CallToolResult {
2435 content: vec![Content::Text {
2436 text: serde_json::to_string_pretty(&response)
2437 .map_err(|e| McpError::serialization_failed("bulk_update_dates response", e))?,
2438 }],
2439 is_error: false,
2440 })
2441 }
2442
2443 async fn handle_bulk_complete(&self, args: Value) -> McpResult<CallToolResult> {
2444 let task_uuid_strs: Vec<String> = args
2446 .get("task_uuids")
2447 .and_then(|v| v.as_array())
2448 .ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
2449 .iter()
2450 .filter_map(|v| v.as_str().map(String::from))
2451 .collect();
2452
2453 let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
2454 .iter()
2455 .map(|s| {
2456 uuid::Uuid::parse_str(s).map_err(|e| {
2457 McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
2458 })
2459 })
2460 .collect::<McpResult<Vec<_>>>()?;
2461
2462 let request = things3_core::models::BulkCompleteRequest { task_uuids };
2463
2464 let result = self
2465 .db
2466 .bulk_complete(request)
2467 .await
2468 .map_err(|e| McpError::database_operation_failed("bulk_complete", e))?;
2469
2470 let response = serde_json::json!({
2471 "success": result.success,
2472 "processed_count": result.processed_count,
2473 "message": result.message
2474 });
2475
2476 Ok(CallToolResult {
2477 content: vec![Content::Text {
2478 text: serde_json::to_string_pretty(&response)
2479 .map_err(|e| McpError::serialization_failed("bulk_complete response", e))?,
2480 }],
2481 is_error: false,
2482 })
2483 }
2484
2485 async fn handle_bulk_delete(&self, args: Value) -> McpResult<CallToolResult> {
2486 let task_uuid_strs: Vec<String> = args
2488 .get("task_uuids")
2489 .and_then(|v| v.as_array())
2490 .ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
2491 .iter()
2492 .filter_map(|v| v.as_str().map(String::from))
2493 .collect();
2494
2495 let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
2496 .iter()
2497 .map(|s| {
2498 uuid::Uuid::parse_str(s).map_err(|e| {
2499 McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
2500 })
2501 })
2502 .collect::<McpResult<Vec<_>>>()?;
2503
2504 let request = things3_core::models::BulkDeleteRequest { task_uuids };
2505
2506 let result = self
2507 .db
2508 .bulk_delete(request)
2509 .await
2510 .map_err(|e| McpError::database_operation_failed("bulk_delete", e))?;
2511
2512 let response = serde_json::json!({
2513 "success": result.success,
2514 "processed_count": result.processed_count,
2515 "message": result.message
2516 });
2517
2518 Ok(CallToolResult {
2519 content: vec![Content::Text {
2520 text: serde_json::to_string_pretty(&response)
2521 .map_err(|e| McpError::serialization_failed("bulk_delete response", e))?,
2522 }],
2523 is_error: false,
2524 })
2525 }
2526
2527 async fn handle_create_project(&self, args: Value) -> McpResult<CallToolResult> {
2528 let title = args
2529 .get("title")
2530 .and_then(|v| v.as_str())
2531 .ok_or_else(|| McpError::invalid_parameter("title", "Project title is required"))?
2532 .to_string();
2533
2534 let notes = args.get("notes").and_then(|v| v.as_str()).map(String::from);
2535
2536 let area_uuid = args
2537 .get("area_uuid")
2538 .and_then(|v| v.as_str())
2539 .map(|s| {
2540 uuid::Uuid::parse_str(s).map_err(|e| {
2541 McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
2542 })
2543 })
2544 .transpose()?;
2545
2546 let start_date = args
2547 .get("start_date")
2548 .and_then(|v| v.as_str())
2549 .map(|s| {
2550 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2551 McpError::invalid_parameter("start_date", format!("Invalid date: {e}"))
2552 })
2553 })
2554 .transpose()?;
2555
2556 let deadline = args
2557 .get("deadline")
2558 .and_then(|v| v.as_str())
2559 .map(|s| {
2560 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2561 McpError::invalid_parameter("deadline", format!("Invalid date: {e}"))
2562 })
2563 })
2564 .transpose()?;
2565
2566 let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
2567 arr.iter()
2568 .filter_map(|v| v.as_str().map(String::from))
2569 .collect::<Vec<_>>()
2570 });
2571
2572 let request = things3_core::models::CreateProjectRequest {
2573 title,
2574 notes,
2575 area_uuid,
2576 start_date,
2577 deadline,
2578 tags,
2579 };
2580
2581 let uuid = self
2582 .db
2583 .create_project(request)
2584 .await
2585 .map_err(|e| McpError::database_operation_failed("create_project", e))?;
2586
2587 let response = serde_json::json!({
2588 "message": "Project created successfully",
2589 "uuid": uuid.to_string()
2590 });
2591
2592 Ok(CallToolResult {
2593 content: vec![Content::Text {
2594 text: serde_json::to_string_pretty(&response)
2595 .map_err(|e| McpError::serialization_failed("create_project response", e))?,
2596 }],
2597 is_error: false,
2598 })
2599 }
2600
2601 async fn handle_update_project(&self, args: Value) -> McpResult<CallToolResult> {
2602 let uuid_str = args
2603 .get("uuid")
2604 .and_then(|v| v.as_str())
2605 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2606
2607 let uuid = uuid::Uuid::parse_str(uuid_str)
2608 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2609
2610 let title = args.get("title").and_then(|v| v.as_str()).map(String::from);
2611 let notes = args.get("notes").and_then(|v| v.as_str()).map(String::from);
2612
2613 let area_uuid = args
2614 .get("area_uuid")
2615 .and_then(|v| v.as_str())
2616 .map(|s| {
2617 uuid::Uuid::parse_str(s).map_err(|e| {
2618 McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
2619 })
2620 })
2621 .transpose()?;
2622
2623 let start_date = args
2624 .get("start_date")
2625 .and_then(|v| v.as_str())
2626 .map(|s| {
2627 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2628 McpError::invalid_parameter("start_date", format!("Invalid date: {e}"))
2629 })
2630 })
2631 .transpose()?;
2632
2633 let deadline = args
2634 .get("deadline")
2635 .and_then(|v| v.as_str())
2636 .map(|s| {
2637 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
2638 McpError::invalid_parameter("deadline", format!("Invalid date: {e}"))
2639 })
2640 })
2641 .transpose()?;
2642
2643 let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
2644 arr.iter()
2645 .filter_map(|v| v.as_str().map(String::from))
2646 .collect::<Vec<_>>()
2647 });
2648
2649 let request = things3_core::models::UpdateProjectRequest {
2650 uuid,
2651 title,
2652 notes,
2653 area_uuid,
2654 start_date,
2655 deadline,
2656 tags,
2657 };
2658
2659 self.db
2660 .update_project(request)
2661 .await
2662 .map_err(|e| McpError::database_operation_failed("update_project", e))?;
2663
2664 let response = serde_json::json!({
2665 "message": "Project updated successfully",
2666 "uuid": uuid_str
2667 });
2668
2669 Ok(CallToolResult {
2670 content: vec![Content::Text {
2671 text: serde_json::to_string_pretty(&response)
2672 .map_err(|e| McpError::serialization_failed("update_project response", e))?,
2673 }],
2674 is_error: false,
2675 })
2676 }
2677
2678 async fn handle_complete_project(&self, args: Value) -> McpResult<CallToolResult> {
2679 let uuid_str = args
2680 .get("uuid")
2681 .and_then(|v| v.as_str())
2682 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2683
2684 let uuid = uuid::Uuid::parse_str(uuid_str)
2685 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2686
2687 let child_handling_str = args
2688 .get("child_handling")
2689 .and_then(|v| v.as_str())
2690 .unwrap_or("error");
2691
2692 let child_handling = match child_handling_str {
2693 "cascade" => things3_core::models::ProjectChildHandling::Cascade,
2694 "orphan" => things3_core::models::ProjectChildHandling::Orphan,
2695 _ => things3_core::models::ProjectChildHandling::Error,
2696 };
2697
2698 self.db
2699 .complete_project(&uuid, child_handling)
2700 .await
2701 .map_err(|e| McpError::database_operation_failed("complete_project", e))?;
2702
2703 let response = serde_json::json!({
2704 "message": "Project completed successfully",
2705 "uuid": uuid_str
2706 });
2707
2708 Ok(CallToolResult {
2709 content: vec![Content::Text {
2710 text: serde_json::to_string_pretty(&response)
2711 .map_err(|e| McpError::serialization_failed("complete_project response", e))?,
2712 }],
2713 is_error: false,
2714 })
2715 }
2716
2717 async fn handle_delete_project(&self, args: Value) -> McpResult<CallToolResult> {
2718 let uuid_str = args
2719 .get("uuid")
2720 .and_then(|v| v.as_str())
2721 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2722
2723 let uuid = uuid::Uuid::parse_str(uuid_str)
2724 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2725
2726 let child_handling_str = args
2727 .get("child_handling")
2728 .and_then(|v| v.as_str())
2729 .unwrap_or("error");
2730
2731 let child_handling = match child_handling_str {
2732 "cascade" => things3_core::models::ProjectChildHandling::Cascade,
2733 "orphan" => things3_core::models::ProjectChildHandling::Orphan,
2734 _ => things3_core::models::ProjectChildHandling::Error,
2735 };
2736
2737 self.db
2738 .delete_project(&uuid, child_handling)
2739 .await
2740 .map_err(|e| McpError::database_operation_failed("delete_project", e))?;
2741
2742 let response = serde_json::json!({
2743 "message": "Project deleted successfully",
2744 "uuid": uuid_str
2745 });
2746
2747 Ok(CallToolResult {
2748 content: vec![Content::Text {
2749 text: serde_json::to_string_pretty(&response)
2750 .map_err(|e| McpError::serialization_failed("delete_project response", e))?,
2751 }],
2752 is_error: false,
2753 })
2754 }
2755
2756 async fn handle_create_area(&self, args: Value) -> McpResult<CallToolResult> {
2757 let title = args
2758 .get("title")
2759 .and_then(|v| v.as_str())
2760 .ok_or_else(|| McpError::invalid_parameter("title", "Area title is required"))?
2761 .to_string();
2762
2763 let request = things3_core::models::CreateAreaRequest { title };
2764
2765 let uuid = self
2766 .db
2767 .create_area(request)
2768 .await
2769 .map_err(|e| McpError::database_operation_failed("create_area", e))?;
2770
2771 let response = serde_json::json!({
2772 "message": "Area created successfully",
2773 "uuid": uuid.to_string()
2774 });
2775
2776 Ok(CallToolResult {
2777 content: vec![Content::Text {
2778 text: serde_json::to_string_pretty(&response)
2779 .map_err(|e| McpError::serialization_failed("create_area response", e))?,
2780 }],
2781 is_error: false,
2782 })
2783 }
2784
2785 async fn handle_update_area(&self, args: Value) -> McpResult<CallToolResult> {
2786 let uuid_str = args
2787 .get("uuid")
2788 .and_then(|v| v.as_str())
2789 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2790
2791 let uuid = uuid::Uuid::parse_str(uuid_str)
2792 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2793
2794 let title = args
2795 .get("title")
2796 .and_then(|v| v.as_str())
2797 .ok_or_else(|| McpError::invalid_parameter("title", "Title is required"))?
2798 .to_string();
2799
2800 let request = things3_core::models::UpdateAreaRequest { uuid, title };
2801
2802 self.db
2803 .update_area(request)
2804 .await
2805 .map_err(|e| McpError::database_operation_failed("update_area", e))?;
2806
2807 let response = serde_json::json!({
2808 "message": "Area updated successfully",
2809 "uuid": uuid_str
2810 });
2811
2812 Ok(CallToolResult {
2813 content: vec![Content::Text {
2814 text: serde_json::to_string_pretty(&response)
2815 .map_err(|e| McpError::serialization_failed("update_area response", e))?,
2816 }],
2817 is_error: false,
2818 })
2819 }
2820
2821 async fn handle_delete_area(&self, args: Value) -> McpResult<CallToolResult> {
2822 let uuid_str = args
2823 .get("uuid")
2824 .and_then(|v| v.as_str())
2825 .ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
2826
2827 let uuid = uuid::Uuid::parse_str(uuid_str)
2828 .map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
2829
2830 self.db
2831 .delete_area(&uuid)
2832 .await
2833 .map_err(|e| McpError::database_operation_failed("delete_area", e))?;
2834
2835 let response = serde_json::json!({
2836 "message": "Area deleted successfully",
2837 "uuid": uuid_str
2838 });
2839
2840 Ok(CallToolResult {
2841 content: vec![Content::Text {
2842 text: serde_json::to_string_pretty(&response)
2843 .map_err(|e| McpError::serialization_failed("delete_area response", e))?,
2844 }],
2845 is_error: false,
2846 })
2847 }
2848
2849 async fn handle_get_productivity_metrics(&self, args: Value) -> McpResult<CallToolResult> {
2850 let days = usize::try_from(
2851 args.get("days")
2852 .and_then(serde_json::Value::as_u64)
2853 .unwrap_or(7),
2854 )
2855 .unwrap_or(7);
2856
2857 let db = &self.db;
2859 let inbox_tasks = db
2860 .get_inbox(None)
2861 .await
2862 .map_err(|e| McpError::database_operation_failed("get_inbox for metrics", e))?;
2863 let today_tasks = db
2864 .get_today(None)
2865 .await
2866 .map_err(|e| McpError::database_operation_failed("get_today for metrics", e))?;
2867 let projects = db
2868 .get_projects(None)
2869 .await
2870 .map_err(|e| McpError::database_operation_failed("get_projects for metrics", e))?;
2871 let areas = db
2872 .get_areas()
2873 .await
2874 .map_err(|e| McpError::database_operation_failed("get_areas for metrics", e))?;
2875 let _ = db;
2876
2877 let metrics = serde_json::json!({
2878 "period_days": days,
2879 "inbox_tasks_count": inbox_tasks.len(),
2880 "today_tasks_count": today_tasks.len(),
2881 "projects_count": projects.len(),
2882 "areas_count": areas.len(),
2883 "completed_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Completed).count(),
2884 "incomplete_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Incomplete).count(),
2885 "timestamp": chrono::Utc::now()
2886 });
2887
2888 Ok(CallToolResult {
2889 content: vec![Content::Text {
2890 text: serde_json::to_string_pretty(&metrics).map_err(|e| {
2891 McpError::serialization_failed("productivity_metrics serialization", e)
2892 })?,
2893 }],
2894 is_error: false,
2895 })
2896 }
2897
2898 async fn handle_export_data(&self, args: Value) -> McpResult<CallToolResult> {
2899 let format = args
2900 .get("format")
2901 .and_then(|v| v.as_str())
2902 .ok_or_else(|| McpError::missing_parameter("format"))?;
2903 let data_type = args
2904 .get("data_type")
2905 .and_then(|v| v.as_str())
2906 .ok_or_else(|| McpError::missing_parameter("data_type"))?;
2907
2908 let db = &self.db;
2909 let export_data =
2910 match data_type {
2911 "tasks" => {
2912 let inbox = db.get_inbox(None).await.map_err(|e| {
2913 McpError::database_operation_failed("get_inbox for export", e)
2914 })?;
2915 let today = db.get_today(None).await.map_err(|e| {
2916 McpError::database_operation_failed("get_today for export", e)
2917 })?;
2918 serde_json::json!({
2919 "inbox": inbox,
2920 "today": today
2921 })
2922 }
2923 "projects" => {
2924 let projects = db.get_projects(None).await.map_err(|e| {
2925 McpError::database_operation_failed("get_projects for export", e)
2926 })?;
2927 serde_json::json!({ "projects": projects })
2928 }
2929 "areas" => {
2930 let areas = db.get_areas().await.map_err(|e| {
2931 McpError::database_operation_failed("get_areas for export", e)
2932 })?;
2933 serde_json::json!({ "areas": areas })
2934 }
2935 "all" => {
2936 let inbox = db.get_inbox(None).await.map_err(|e| {
2937 McpError::database_operation_failed("get_inbox for export", e)
2938 })?;
2939 let today = db.get_today(None).await.map_err(|e| {
2940 McpError::database_operation_failed("get_today for export", e)
2941 })?;
2942 let projects = db.get_projects(None).await.map_err(|e| {
2943 McpError::database_operation_failed("get_projects for export", e)
2944 })?;
2945 let areas = db.get_areas().await.map_err(|e| {
2946 McpError::database_operation_failed("get_areas for export", e)
2947 })?;
2948 let _ = db;
2949 serde_json::json!({
2950 "inbox": inbox,
2951 "today": today,
2952 "projects": projects,
2953 "areas": areas
2954 })
2955 }
2956 _ => {
2957 return Err(McpError::invalid_data_type(
2958 data_type,
2959 "tasks, projects, areas, all",
2960 ))
2961 }
2962 };
2963
2964 let result = match format {
2965 "json" => serde_json::to_string_pretty(&export_data)
2966 .map_err(|e| McpError::serialization_failed("export_data serialization", e))?,
2967 "csv" => "CSV export not yet implemented".to_string(),
2968 "markdown" => "Markdown export not yet implemented".to_string(),
2969 _ => return Err(McpError::invalid_format(format, "json, csv, markdown")),
2970 };
2971
2972 Ok(CallToolResult {
2973 content: vec![Content::Text { text: result }],
2974 is_error: false,
2975 })
2976 }
2977
2978 fn handle_bulk_create_tasks(args: &Value) -> McpResult<CallToolResult> {
2979 let tasks = args
2980 .get("tasks")
2981 .and_then(|v| v.as_array())
2982 .ok_or_else(|| McpError::missing_parameter("tasks"))?;
2983
2984 let response = serde_json::json!({
2985 "message": "Bulk task creation not yet implemented",
2986 "tasks_count": tasks.len(),
2987 "status": "placeholder"
2988 });
2989
2990 Ok(CallToolResult {
2991 content: vec![Content::Text {
2992 text: serde_json::to_string_pretty(&response)
2993 .map_err(|e| McpError::serialization_failed("bulk_create_tasks response", e))?,
2994 }],
2995 is_error: false,
2996 })
2997 }
2998
2999 async fn handle_get_recent_tasks(&self, args: Value) -> McpResult<CallToolResult> {
3000 let limit = args
3001 .get("limit")
3002 .and_then(serde_json::Value::as_u64)
3003 .map(|v| usize::try_from(v).unwrap_or(usize::MAX));
3004 let hours = i64::try_from(
3005 args.get("hours")
3006 .and_then(serde_json::Value::as_u64)
3007 .unwrap_or(24),
3008 )
3009 .unwrap_or(24);
3010
3011 let tasks = self
3014 .db
3015 .get_inbox(limit)
3016 .await
3017 .map_err(|e| McpError::database_operation_failed("get_recent_tasks", e))?;
3018
3019 let response = serde_json::json!({
3020 "message": "Recent tasks (using inbox as proxy)",
3021 "hours_lookback": hours,
3022 "tasks": tasks
3023 });
3024
3025 Ok(CallToolResult {
3026 content: vec![Content::Text {
3027 text: serde_json::to_string_pretty(&response)
3028 .map_err(|e| McpError::serialization_failed("get_recent_tasks response", e))?,
3029 }],
3030 is_error: false,
3031 })
3032 }
3033
3034 async fn handle_backup_database(&self, args: Value) -> McpResult<CallToolResult> {
3035 let backup_dir = args
3036 .get("backup_dir")
3037 .and_then(|v| v.as_str())
3038 .ok_or_else(|| McpError::missing_parameter("backup_dir"))?;
3039 let description = args.get("description").and_then(|v| v.as_str());
3040
3041 let backup_path = std::path::Path::new(backup_dir);
3042 let metadata = self
3043 .backup_manager
3044 .lock()
3045 .await
3046 .create_backup(backup_path, description)
3047 .map_err(|e| {
3048 McpError::backup_operation_failed(
3049 "create_backup",
3050 things3_core::ThingsError::unknown(e.to_string()),
3051 )
3052 })?;
3053
3054 let response = serde_json::json!({
3055 "message": "Backup created successfully",
3056 "backup_path": metadata.backup_path,
3057 "file_size": metadata.file_size,
3058 "created_at": metadata.created_at
3059 });
3060
3061 Ok(CallToolResult {
3062 content: vec![Content::Text {
3063 text: serde_json::to_string_pretty(&response)
3064 .map_err(|e| McpError::serialization_failed("backup_database response", e))?,
3065 }],
3066 is_error: false,
3067 })
3068 }
3069
3070 async fn handle_restore_database(&self, args: Value) -> McpResult<CallToolResult> {
3071 let backup_path = args
3072 .get("backup_path")
3073 .and_then(|v| v.as_str())
3074 .ok_or_else(|| McpError::missing_parameter("backup_path"))?;
3075
3076 let backup_file = std::path::Path::new(backup_path);
3077 self.backup_manager
3078 .lock()
3079 .await
3080 .restore_backup(backup_file)
3081 .map_err(|e| {
3082 McpError::backup_operation_failed(
3083 "restore_backup",
3084 things3_core::ThingsError::unknown(e.to_string()),
3085 )
3086 })?;
3087
3088 let response = serde_json::json!({
3089 "message": "Database restored successfully",
3090 "backup_path": backup_path
3091 });
3092
3093 Ok(CallToolResult {
3094 content: vec![Content::Text {
3095 text: serde_json::to_string_pretty(&response)
3096 .map_err(|e| McpError::serialization_failed("restore_database response", e))?,
3097 }],
3098 is_error: false,
3099 })
3100 }
3101
3102 async fn handle_list_backups(&self, args: Value) -> McpResult<CallToolResult> {
3103 let backup_dir = args
3104 .get("backup_dir")
3105 .and_then(|v| v.as_str())
3106 .ok_or_else(|| McpError::missing_parameter("backup_dir"))?;
3107
3108 let backup_path = std::path::Path::new(backup_dir);
3109 let backups = self
3110 .backup_manager
3111 .lock()
3112 .await
3113 .list_backups(backup_path)
3114 .map_err(|e| {
3115 McpError::backup_operation_failed(
3116 "list_backups",
3117 things3_core::ThingsError::unknown(e.to_string()),
3118 )
3119 })?;
3120
3121 let response = serde_json::json!({
3122 "backups": backups,
3123 "count": backups.len()
3124 });
3125
3126 Ok(CallToolResult {
3127 content: vec![Content::Text {
3128 text: serde_json::to_string_pretty(&response)
3129 .map_err(|e| McpError::serialization_failed("list_backups response", e))?,
3130 }],
3131 is_error: false,
3132 })
3133 }
3134
3135 async fn handle_get_performance_stats(&self, _args: Value) -> McpResult<CallToolResult> {
3136 let monitor = self.performance_monitor.lock().await;
3137 let stats = monitor.get_all_stats();
3138 let summary = monitor.get_summary();
3139 drop(monitor);
3140
3141 let response = serde_json::json!({
3142 "summary": summary,
3143 "operation_stats": stats
3144 });
3145
3146 Ok(CallToolResult {
3147 content: vec![Content::Text {
3148 text: serde_json::to_string_pretty(&response)
3149 .map_err(|e| McpError::serialization_failed("performance_stats response", e))?,
3150 }],
3151 is_error: false,
3152 })
3153 }
3154
3155 async fn handle_get_system_metrics(&self, _args: Value) -> McpResult<CallToolResult> {
3156 let metrics = self
3157 .performance_monitor
3158 .lock()
3159 .await
3160 .get_system_metrics()
3161 .map_err(|e| {
3162 McpError::performance_monitoring_failed(
3163 "get_system_metrics",
3164 things3_core::ThingsError::unknown(e.to_string()),
3165 )
3166 })?;
3167
3168 Ok(CallToolResult {
3169 content: vec![Content::Text {
3170 text: serde_json::to_string_pretty(&metrics)
3171 .map_err(|e| McpError::serialization_failed("system_metrics response", e))?,
3172 }],
3173 is_error: false,
3174 })
3175 }
3176
3177 async fn handle_get_cache_stats(&self, _args: Value) -> McpResult<CallToolResult> {
3178 let stats = self.cache.lock().await.get_stats();
3179
3180 Ok(CallToolResult {
3181 content: vec![Content::Text {
3182 text: serde_json::to_string_pretty(&stats)
3183 .map_err(|e| McpError::serialization_failed("cache_stats response", e))?,
3184 }],
3185 is_error: false,
3186 })
3187 }
3188
3189 async fn handle_search_tags_tool(&self, args: Value) -> McpResult<CallToolResult> {
3194 let query: String = args
3195 .get("query")
3196 .and_then(|v| v.as_str())
3197 .ok_or_else(|| McpError::invalid_parameter("query", "Missing 'query' parameter"))?
3198 .to_string();
3199
3200 let include_similar = args
3201 .get("include_similar")
3202 .and_then(|v| v.as_bool())
3203 .unwrap_or(true);
3204
3205 let min_similarity = args
3206 .get("min_similarity")
3207 .and_then(|v| v.as_f64())
3208 .unwrap_or(0.7) as f32;
3209
3210 let tags = if include_similar {
3211 self.db
3212 .find_similar_tags(&query, min_similarity)
3213 .await
3214 .map_err(|e| McpError::database_operation_failed("search_tags", e))?
3215 .into_iter()
3216 .map(|tm| tm.tag)
3217 .collect()
3218 } else {
3219 self.db
3220 .search_tags(&query)
3221 .await
3222 .map_err(|e| McpError::database_operation_failed("search_tags", e))?
3223 };
3224
3225 Ok(CallToolResult {
3226 content: vec![Content::Text {
3227 text: serde_json::to_string_pretty(&tags)
3228 .map_err(|e| McpError::serialization_failed("tags", e))?,
3229 }],
3230 is_error: false,
3231 })
3232 }
3233
3234 async fn handle_get_tag_suggestions(&self, args: Value) -> McpResult<CallToolResult> {
3235 let title: String = args
3236 .get("title")
3237 .and_then(|v| v.as_str())
3238 .ok_or_else(|| McpError::invalid_parameter("title", "Missing 'title' parameter"))?
3239 .to_string();
3240
3241 use things3_core::database::tag_utils::normalize_tag_title;
3242 let normalized = normalize_tag_title(&title);
3243
3244 let exact_match = self
3246 .db
3247 .find_tag_by_normalized_title(&normalized)
3248 .await
3249 .map_err(|e| McpError::database_operation_failed("get_tag_suggestions", e))?;
3250
3251 let similar_tags = self
3253 .db
3254 .find_similar_tags(&normalized, 0.7)
3255 .await
3256 .map_err(|e| McpError::database_operation_failed("get_tag_suggestions", e))?;
3257
3258 let recommendation = if exact_match.is_some() {
3259 "use_existing"
3260 } else if !similar_tags.is_empty() {
3261 "consider_similar"
3262 } else {
3263 "create_new"
3264 };
3265
3266 let response = serde_json::json!({
3267 "exact_match": exact_match,
3268 "similar_tags": similar_tags,
3269 "recommendation": recommendation
3270 });
3271
3272 Ok(CallToolResult {
3273 content: vec![Content::Text {
3274 text: serde_json::to_string_pretty(&response)
3275 .map_err(|e| McpError::serialization_failed("tag_suggestions", e))?,
3276 }],
3277 is_error: false,
3278 })
3279 }
3280
3281 async fn handle_get_popular_tags(&self, args: Value) -> McpResult<CallToolResult> {
3282 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
3283
3284 let tags = self
3285 .db
3286 .get_popular_tags(limit)
3287 .await
3288 .map_err(|e| McpError::database_operation_failed("get_popular_tags", e))?;
3289
3290 Ok(CallToolResult {
3291 content: vec![Content::Text {
3292 text: serde_json::to_string_pretty(&tags)
3293 .map_err(|e| McpError::serialization_failed("popular_tags", e))?,
3294 }],
3295 is_error: false,
3296 })
3297 }
3298
3299 async fn handle_get_recent_tags(&self, args: Value) -> McpResult<CallToolResult> {
3300 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
3301
3302 let tags = self
3303 .db
3304 .get_recent_tags(limit)
3305 .await
3306 .map_err(|e| McpError::database_operation_failed("get_recent_tags", e))?;
3307
3308 Ok(CallToolResult {
3309 content: vec![Content::Text {
3310 text: serde_json::to_string_pretty(&tags)
3311 .map_err(|e| McpError::serialization_failed("recent_tags", e))?,
3312 }],
3313 is_error: false,
3314 })
3315 }
3316
3317 async fn handle_create_tag(&self, args: Value) -> McpResult<CallToolResult> {
3318 let title: String = args
3319 .get("title")
3320 .and_then(|v| v.as_str())
3321 .ok_or_else(|| McpError::invalid_parameter("title", "Missing 'title' parameter"))?
3322 .to_string();
3323
3324 let shortcut: Option<String> = args
3325 .get("shortcut")
3326 .and_then(|v| v.as_str())
3327 .map(|s| s.to_string());
3328
3329 let parent_uuid: Option<Uuid> = args
3330 .get("parent_uuid")
3331 .and_then(|v| v.as_str())
3332 .and_then(|s| Uuid::parse_str(s).ok());
3333
3334 let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
3335
3336 let request = things3_core::models::CreateTagRequest {
3337 title,
3338 shortcut,
3339 parent_uuid,
3340 };
3341
3342 let result = if force {
3343 let uuid = self
3344 .db
3345 .create_tag_force(request)
3346 .await
3347 .map_err(|e| McpError::database_operation_failed("create_tag", e))?;
3348 serde_json::json!({
3349 "status": "created",
3350 "uuid": uuid,
3351 "message": "Tag created successfully (duplicate check skipped)"
3352 })
3353 } else {
3354 match self
3355 .db
3356 .create_tag_smart(request)
3357 .await
3358 .map_err(|e| McpError::database_operation_failed("create_tag", e))?
3359 {
3360 things3_core::models::TagCreationResult::Created { uuid, .. } => {
3361 serde_json::json!({
3362 "status": "created",
3363 "uuid": uuid,
3364 "message": "Tag created successfully"
3365 })
3366 }
3367 things3_core::models::TagCreationResult::Existing { tag, .. } => {
3368 serde_json::json!({
3369 "status": "existing",
3370 "uuid": tag.uuid,
3371 "tag": tag,
3372 "message": "Tag already exists"
3373 })
3374 }
3375 things3_core::models::TagCreationResult::SimilarFound {
3376 similar_tags,
3377 requested_title,
3378 } => {
3379 serde_json::json!({
3380 "status": "similar_found",
3381 "similar_tags": similar_tags,
3382 "requested_title": requested_title,
3383 "message": "Similar tags found. Use force=true to create anyway."
3384 })
3385 }
3386 }
3387 };
3388
3389 Ok(CallToolResult {
3390 content: vec![Content::Text {
3391 text: serde_json::to_string_pretty(&result)
3392 .map_err(|e| McpError::serialization_failed("create_tag_response", e))?,
3393 }],
3394 is_error: false,
3395 })
3396 }
3397
3398 async fn handle_update_tag(&self, args: Value) -> McpResult<CallToolResult> {
3399 let uuid: Uuid = args
3400 .get("uuid")
3401 .and_then(|v| v.as_str())
3402 .and_then(|s| Uuid::parse_str(s).ok())
3403 .ok_or_else(|| {
3404 McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
3405 })?;
3406
3407 let title: Option<String> = args
3408 .get("title")
3409 .and_then(|v| v.as_str())
3410 .map(|s| s.to_string());
3411
3412 let shortcut: Option<String> = args
3413 .get("shortcut")
3414 .and_then(|v| v.as_str())
3415 .map(|s| s.to_string());
3416
3417 let parent_uuid: Option<Uuid> = args
3418 .get("parent_uuid")
3419 .and_then(|v| v.as_str())
3420 .and_then(|s| Uuid::parse_str(s).ok());
3421
3422 let request = things3_core::models::UpdateTagRequest {
3423 uuid,
3424 title,
3425 shortcut,
3426 parent_uuid,
3427 };
3428
3429 self.db
3430 .update_tag(request)
3431 .await
3432 .map_err(|e| McpError::database_operation_failed("update_tag", e))?;
3433
3434 let response = serde_json::json!({
3435 "message": "Tag updated successfully",
3436 "uuid": uuid
3437 });
3438
3439 Ok(CallToolResult {
3440 content: vec![Content::Text {
3441 text: serde_json::to_string_pretty(&response)
3442 .map_err(|e| McpError::serialization_failed("update_tag_response", e))?,
3443 }],
3444 is_error: false,
3445 })
3446 }
3447
3448 async fn handle_delete_tag(&self, args: Value) -> McpResult<CallToolResult> {
3449 let uuid: Uuid = args
3450 .get("uuid")
3451 .and_then(|v| v.as_str())
3452 .and_then(|s| Uuid::parse_str(s).ok())
3453 .ok_or_else(|| {
3454 McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
3455 })?;
3456
3457 let remove_from_tasks = args
3458 .get("remove_from_tasks")
3459 .and_then(|v| v.as_bool())
3460 .unwrap_or(false);
3461
3462 self.db
3463 .delete_tag(&uuid, remove_from_tasks)
3464 .await
3465 .map_err(|e| McpError::database_operation_failed("delete_tag", e))?;
3466
3467 let response = serde_json::json!({
3468 "message": "Tag deleted successfully",
3469 "uuid": uuid
3470 });
3471
3472 Ok(CallToolResult {
3473 content: vec![Content::Text {
3474 text: serde_json::to_string_pretty(&response)
3475 .map_err(|e| McpError::serialization_failed("delete_tag_response", e))?,
3476 }],
3477 is_error: false,
3478 })
3479 }
3480
3481 async fn handle_merge_tags(&self, args: Value) -> McpResult<CallToolResult> {
3482 let source_uuid: Uuid = args
3483 .get("source_uuid")
3484 .and_then(|v| v.as_str())
3485 .and_then(|s| Uuid::parse_str(s).ok())
3486 .ok_or_else(|| {
3487 McpError::invalid_parameter(
3488 "source_uuid",
3489 "Missing or invalid 'source_uuid' parameter",
3490 )
3491 })?;
3492
3493 let target_uuid: Uuid = args
3494 .get("target_uuid")
3495 .and_then(|v| v.as_str())
3496 .and_then(|s| Uuid::parse_str(s).ok())
3497 .ok_or_else(|| {
3498 McpError::invalid_parameter(
3499 "target_uuid",
3500 "Missing or invalid 'target_uuid' parameter",
3501 )
3502 })?;
3503
3504 self.db
3505 .merge_tags(&source_uuid, &target_uuid)
3506 .await
3507 .map_err(|e| McpError::database_operation_failed("merge_tags", e))?;
3508
3509 let response = serde_json::json!({
3510 "message": "Tags merged successfully",
3511 "source_uuid": source_uuid,
3512 "target_uuid": target_uuid
3513 });
3514
3515 Ok(CallToolResult {
3516 content: vec![Content::Text {
3517 text: serde_json::to_string_pretty(&response)
3518 .map_err(|e| McpError::serialization_failed("merge_tags_response", e))?,
3519 }],
3520 is_error: false,
3521 })
3522 }
3523
3524 async fn handle_add_tag_to_task(&self, args: Value) -> McpResult<CallToolResult> {
3525 let task_uuid: Uuid = args
3526 .get("task_uuid")
3527 .and_then(|v| v.as_str())
3528 .and_then(|s| Uuid::parse_str(s).ok())
3529 .ok_or_else(|| {
3530 McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
3531 })?;
3532
3533 let tag_title: String = args
3534 .get("tag_title")
3535 .and_then(|v| v.as_str())
3536 .ok_or_else(|| {
3537 McpError::invalid_parameter("tag_title", "Missing 'tag_title' parameter")
3538 })?
3539 .to_string();
3540
3541 let result = self
3542 .db
3543 .add_tag_to_task(&task_uuid, &tag_title)
3544 .await
3545 .map_err(|e| McpError::database_operation_failed("add_tag_to_task", e))?;
3546
3547 let response = match result {
3548 things3_core::models::TagAssignmentResult::Assigned { tag_uuid } => {
3549 serde_json::json!({
3550 "status": "assigned",
3551 "tag_uuid": tag_uuid,
3552 "message": "Tag added to task successfully"
3553 })
3554 }
3555 things3_core::models::TagAssignmentResult::Suggestions { similar_tags } => {
3556 serde_json::json!({
3557 "status": "suggestions",
3558 "similar_tags": similar_tags,
3559 "message": "Similar tags found. Please confirm or use a different tag."
3560 })
3561 }
3562 };
3563
3564 Ok(CallToolResult {
3565 content: vec![Content::Text {
3566 text: serde_json::to_string_pretty(&response)
3567 .map_err(|e| McpError::serialization_failed("add_tag_to_task_response", e))?,
3568 }],
3569 is_error: false,
3570 })
3571 }
3572
3573 async fn handle_remove_tag_from_task(&self, args: Value) -> McpResult<CallToolResult> {
3574 let task_uuid: Uuid = args
3575 .get("task_uuid")
3576 .and_then(|v| v.as_str())
3577 .and_then(|s| Uuid::parse_str(s).ok())
3578 .ok_or_else(|| {
3579 McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
3580 })?;
3581
3582 let tag_title: String = args
3583 .get("tag_title")
3584 .and_then(|v| v.as_str())
3585 .ok_or_else(|| {
3586 McpError::invalid_parameter("tag_title", "Missing 'tag_title' parameter")
3587 })?
3588 .to_string();
3589
3590 self.db
3591 .remove_tag_from_task(&task_uuid, &tag_title)
3592 .await
3593 .map_err(|e| McpError::database_operation_failed("remove_tag_from_task", e))?;
3594
3595 let response = serde_json::json!({
3596 "message": "Tag removed from task successfully",
3597 "task_uuid": task_uuid,
3598 "tag_title": tag_title
3599 });
3600
3601 Ok(CallToolResult {
3602 content: vec![Content::Text {
3603 text: serde_json::to_string_pretty(&response).map_err(|e| {
3604 McpError::serialization_failed("remove_tag_from_task_response", e)
3605 })?,
3606 }],
3607 is_error: false,
3608 })
3609 }
3610
3611 async fn handle_set_task_tags(&self, args: Value) -> McpResult<CallToolResult> {
3612 let task_uuid: Uuid = args
3613 .get("task_uuid")
3614 .and_then(|v| v.as_str())
3615 .and_then(|s| Uuid::parse_str(s).ok())
3616 .ok_or_else(|| {
3617 McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
3618 })?;
3619
3620 let tag_titles: Vec<String> = args
3621 .get("tag_titles")
3622 .and_then(|v| v.as_array())
3623 .ok_or_else(|| {
3624 McpError::invalid_parameter("tag_titles", "Missing 'tag_titles' parameter")
3625 })?
3626 .iter()
3627 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3628 .collect();
3629
3630 let suggestions = self
3631 .db
3632 .set_task_tags(&task_uuid, tag_titles.clone())
3633 .await
3634 .map_err(|e| McpError::database_operation_failed("set_task_tags", e))?;
3635
3636 let response = serde_json::json!({
3637 "message": "Task tags updated successfully",
3638 "task_uuid": task_uuid,
3639 "tags": tag_titles,
3640 "suggestions": suggestions
3641 });
3642
3643 Ok(CallToolResult {
3644 content: vec![Content::Text {
3645 text: serde_json::to_string_pretty(&response)
3646 .map_err(|e| McpError::serialization_failed("set_task_tags_response", e))?,
3647 }],
3648 is_error: false,
3649 })
3650 }
3651
3652 async fn handle_get_tag_statistics(&self, args: Value) -> McpResult<CallToolResult> {
3653 let uuid: Uuid = args
3654 .get("uuid")
3655 .and_then(|v| v.as_str())
3656 .and_then(|s| Uuid::parse_str(s).ok())
3657 .ok_or_else(|| {
3658 McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
3659 })?;
3660
3661 let stats = self
3662 .db
3663 .get_tag_statistics(&uuid)
3664 .await
3665 .map_err(|e| McpError::database_operation_failed("get_tag_statistics", e))?;
3666
3667 Ok(CallToolResult {
3668 content: vec![Content::Text {
3669 text: serde_json::to_string_pretty(&stats)
3670 .map_err(|e| McpError::serialization_failed("tag_statistics", e))?,
3671 }],
3672 is_error: false,
3673 })
3674 }
3675
3676 async fn handle_find_duplicate_tags(&self, args: Value) -> McpResult<CallToolResult> {
3677 let min_similarity = args
3678 .get("min_similarity")
3679 .and_then(|v| v.as_f64())
3680 .unwrap_or(0.85) as f32;
3681
3682 let duplicates = self
3683 .db
3684 .find_duplicate_tags(min_similarity)
3685 .await
3686 .map_err(|e| McpError::database_operation_failed("find_duplicate_tags", e))?;
3687
3688 Ok(CallToolResult {
3689 content: vec![Content::Text {
3690 text: serde_json::to_string_pretty(&duplicates)
3691 .map_err(|e| McpError::serialization_failed("duplicate_tags", e))?,
3692 }],
3693 is_error: false,
3694 })
3695 }
3696
3697 async fn handle_get_tag_completions(&self, args: Value) -> McpResult<CallToolResult> {
3698 let partial_input: String = args
3699 .get("partial_input")
3700 .and_then(|v| v.as_str())
3701 .ok_or_else(|| {
3702 McpError::invalid_parameter("partial_input", "Missing 'partial_input' parameter")
3703 })?
3704 .to_string();
3705
3706 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
3707
3708 let completions = self
3709 .db
3710 .get_tag_completions(&partial_input, limit)
3711 .await
3712 .map_err(|e| McpError::database_operation_failed("get_tag_completions", e))?;
3713
3714 Ok(CallToolResult {
3715 content: vec![Content::Text {
3716 text: serde_json::to_string_pretty(&completions)
3717 .map_err(|e| McpError::serialization_failed("tag_completions", e))?,
3718 }],
3719 is_error: false,
3720 })
3721 }
3722
3723 fn get_available_prompts() -> Vec<Prompt> {
3725 vec![
3726 Self::create_task_review_prompt(),
3727 Self::create_project_planning_prompt(),
3728 Self::create_productivity_analysis_prompt(),
3729 Self::create_backup_strategy_prompt(),
3730 ]
3731 }
3732
3733 fn create_task_review_prompt() -> Prompt {
3735 Prompt {
3736 name: "task_review".to_string(),
3737 description: "Review task for completeness and clarity".to_string(),
3738 arguments: serde_json::json!({
3739 "type": "object",
3740 "properties": {
3741 "task_title": {
3742 "type": "string",
3743 "description": "The title of the task to review"
3744 },
3745 "task_notes": {
3746 "type": "string",
3747 "description": "Optional notes or description of the task"
3748 },
3749 "context": {
3750 "type": "string",
3751 "description": "Optional context about the task or project"
3752 }
3753 },
3754 "required": ["task_title"]
3755 }),
3756 }
3757 }
3758
3759 fn create_project_planning_prompt() -> Prompt {
3761 Prompt {
3762 name: "project_planning".to_string(),
3763 description: "Help plan projects with tasks and deadlines".to_string(),
3764 arguments: serde_json::json!({
3765 "type": "object",
3766 "properties": {
3767 "project_title": {
3768 "type": "string",
3769 "description": "The title of the project to plan"
3770 },
3771 "project_description": {
3772 "type": "string",
3773 "description": "Description of what the project aims to achieve"
3774 },
3775 "deadline": {
3776 "type": "string",
3777 "description": "Optional deadline for the project"
3778 },
3779 "complexity": {
3780 "type": "string",
3781 "description": "Project complexity level",
3782 "enum": ["simple", "medium", "complex"]
3783 }
3784 },
3785 "required": ["project_title"]
3786 }),
3787 }
3788 }
3789
3790 fn create_productivity_analysis_prompt() -> Prompt {
3792 Prompt {
3793 name: "productivity_analysis".to_string(),
3794 description: "Analyze productivity patterns".to_string(),
3795 arguments: serde_json::json!({
3796 "type": "object",
3797 "properties": {
3798 "time_period": {
3799 "type": "string",
3800 "description": "Time period to analyze",
3801 "enum": ["week", "month", "quarter", "year"]
3802 },
3803 "focus_area": {
3804 "type": "string",
3805 "description": "Specific area to focus analysis on",
3806 "enum": ["completion_rate", "time_management", "task_distribution", "all"]
3807 },
3808 "include_recommendations": {
3809 "type": "boolean",
3810 "description": "Whether to include improvement recommendations"
3811 }
3812 },
3813 "required": ["time_period"]
3814 }),
3815 }
3816 }
3817
3818 fn create_backup_strategy_prompt() -> Prompt {
3820 Prompt {
3821 name: "backup_strategy".to_string(),
3822 description: "Suggest backup strategies".to_string(),
3823 arguments: serde_json::json!({
3824 "type": "object",
3825 "properties": {
3826 "data_volume": {
3827 "type": "string",
3828 "description": "Estimated data volume",
3829 "enum": ["small", "medium", "large"]
3830 },
3831 "frequency": {
3832 "type": "string",
3833 "description": "Desired backup frequency",
3834 "enum": ["daily", "weekly", "monthly"]
3835 },
3836 "retention_period": {
3837 "type": "string",
3838 "description": "How long to keep backups",
3839 "enum": ["1_month", "3_months", "6_months", "1_year", "indefinite"]
3840 },
3841 "storage_preference": {
3842 "type": "string",
3843 "description": "Preferred storage type",
3844 "enum": ["local", "cloud", "hybrid"]
3845 }
3846 },
3847 "required": ["data_volume", "frequency"]
3848 }),
3849 }
3850 }
3851
3852 async fn handle_prompt_request(&self, request: GetPromptRequest) -> McpResult<GetPromptResult> {
3854 let prompt_name = &request.name;
3855 let arguments = request.arguments.unwrap_or_default();
3856
3857 match prompt_name.as_str() {
3858 "task_review" => self.handle_task_review_prompt(arguments).await,
3859 "project_planning" => self.handle_project_planning_prompt(arguments).await,
3860 "productivity_analysis" => self.handle_productivity_analysis_prompt(arguments).await,
3861 "backup_strategy" => self.handle_backup_strategy_prompt(arguments).await,
3862 _ => Err(McpError::prompt_not_found(prompt_name)),
3863 }
3864 }
3865
3866 async fn handle_task_review_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
3868 let task_title = args
3869 .get("task_title")
3870 .and_then(|v| v.as_str())
3871 .ok_or_else(|| McpError::missing_parameter("task_title"))?;
3872 let task_notes = args.get("task_notes").and_then(|v| v.as_str());
3873 let context = args.get("context").and_then(|v| v.as_str());
3874
3875 let db = &self.db;
3877 let inbox_tasks = db
3878 .get_inbox(Some(5))
3879 .await
3880 .map_err(|e| McpError::database_operation_failed("get_inbox for task_review", e))?;
3881 let today_tasks = db
3882 .get_today(Some(5))
3883 .await
3884 .map_err(|e| McpError::database_operation_failed("get_today for task_review", e))?;
3885 let _ = db;
3886
3887 let prompt_text = format!(
3888 "# Task Review: {}\n\n\
3889 ## Current Task Details\n\
3890 - **Title**: {}\n\
3891 - **Notes**: {}\n\
3892 - **Context**: {}\n\n\
3893 ## Review Checklist\n\
3894 Please review this task for:\n\
3895 1. **Clarity**: Is the task title clear and actionable?\n\
3896 2. **Completeness**: Does it have all necessary details?\n\
3897 3. **Priority**: How urgent/important is this task?\n\
3898 4. **Dependencies**: Are there any prerequisites?\n\
3899 5. **Time Estimate**: How long should this take?\n\n\
3900 ## Current Context\n\
3901 - **Inbox Tasks**: {} tasks\n\
3902 - **Today's Tasks**: {} tasks\n\n\
3903 ## Recommendations\n\
3904 Based on the current workload and task details, provide specific recommendations for:\n\
3905 - Improving task clarity\n\
3906 - Breaking down complex tasks\n\
3907 - Setting appropriate deadlines\n\
3908 - Managing dependencies\n\n\
3909 ## Next Steps\n\
3910 Suggest concrete next steps to move this task forward effectively.",
3911 task_title,
3912 task_title,
3913 task_notes.unwrap_or("No notes provided"),
3914 context.unwrap_or("No additional context"),
3915 inbox_tasks.len(),
3916 today_tasks.len()
3917 );
3918
3919 Ok(GetPromptResult {
3920 content: vec![Content::Text { text: prompt_text }],
3921 is_error: false,
3922 })
3923 }
3924
3925 async fn handle_project_planning_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
3927 let project_title = args
3928 .get("project_title")
3929 .and_then(|v| v.as_str())
3930 .ok_or_else(|| McpError::missing_parameter("project_title"))?;
3931 let project_description = args.get("project_description").and_then(|v| v.as_str());
3932 let deadline = args.get("deadline").and_then(|v| v.as_str());
3933 let complexity = args
3934 .get("complexity")
3935 .and_then(|v| v.as_str())
3936 .unwrap_or("medium");
3937
3938 let db = &self.db;
3940 let projects = db.get_projects(None).await.map_err(|e| {
3941 McpError::database_operation_failed("get_projects for project_planning", e)
3942 })?;
3943 let areas = db.get_areas().await.map_err(|e| {
3944 McpError::database_operation_failed("get_areas for project_planning", e)
3945 })?;
3946 let _ = db;
3947
3948 let prompt_text = format!(
3949 "# Project Planning: {}\n\n\
3950 ## Project Overview\n\
3951 - **Title**: {}\n\
3952 - **Description**: {}\n\
3953 - **Deadline**: {}\n\
3954 - **Complexity**: {}\n\n\
3955 ## Planning Framework\n\
3956 Please help plan this project by:\n\
3957 1. **Breaking down** the project into manageable tasks\n\
3958 2. **Estimating** time requirements for each task\n\
3959 3. **Identifying** dependencies between tasks\n\
3960 4. **Suggesting** milestones and checkpoints\n\
3961 5. **Recommending** project organization (areas, tags, etc.)\n\n\
3962 ## Current Context\n\
3963 - **Existing Projects**: {} projects\n\
3964 - **Available Areas**: {} areas\n\n\
3965 ## Task Breakdown\n\
3966 Create a detailed task list with:\n\
3967 - Clear, actionable task titles\n\
3968 - Estimated time for each task\n\
3969 - Priority levels\n\
3970 - Dependencies\n\
3971 - Suggested deadlines\n\n\
3972 ## Project Organization\n\
3973 Suggest:\n\
3974 - Appropriate area for this project\n\
3975 - Useful tags for organization\n\
3976 - Project structure and hierarchy\n\n\
3977 ## Risk Assessment\n\
3978 Identify potential challenges and mitigation strategies.\n\n\
3979 ## Success Metrics\n\
3980 Define how to measure project success and completion.",
3981 project_title,
3982 project_title,
3983 project_description.unwrap_or("No description provided"),
3984 deadline.unwrap_or("No deadline specified"),
3985 complexity,
3986 projects.len(),
3987 areas.len()
3988 );
3989
3990 Ok(GetPromptResult {
3991 content: vec![Content::Text { text: prompt_text }],
3992 is_error: false,
3993 })
3994 }
3995
3996 async fn handle_productivity_analysis_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
3998 let time_period = args
3999 .get("time_period")
4000 .and_then(|v| v.as_str())
4001 .ok_or_else(|| McpError::missing_parameter("time_period"))?;
4002 let focus_area = args
4003 .get("focus_area")
4004 .and_then(|v| v.as_str())
4005 .unwrap_or("all");
4006 let include_recommendations = args
4007 .get("include_recommendations")
4008 .and_then(serde_json::Value::as_bool)
4009 .unwrap_or(true);
4010
4011 let db = &self.db;
4013 let inbox_tasks = db.get_inbox(None).await.map_err(|e| {
4014 McpError::database_operation_failed("get_inbox for productivity_analysis", e)
4015 })?;
4016 let today_tasks = db.get_today(None).await.map_err(|e| {
4017 McpError::database_operation_failed("get_today for productivity_analysis", e)
4018 })?;
4019 let projects = db.get_projects(None).await.map_err(|e| {
4020 McpError::database_operation_failed("get_projects for productivity_analysis", e)
4021 })?;
4022 let areas = db.get_areas().await.map_err(|e| {
4023 McpError::database_operation_failed("get_areas for productivity_analysis", e)
4024 })?;
4025 let _ = db;
4026
4027 let completed_tasks = projects
4028 .iter()
4029 .filter(|p| p.status == things3_core::TaskStatus::Completed)
4030 .count();
4031 let incomplete_tasks = projects
4032 .iter()
4033 .filter(|p| p.status == things3_core::TaskStatus::Incomplete)
4034 .count();
4035
4036 let prompt_text = format!(
4037 "# Productivity Analysis - {}\n\n\
4038 ## Analysis Period: {}\n\
4039 ## Focus Area: {}\n\n\
4040 ## Current Data Overview\n\
4041 - **Inbox Tasks**: {} tasks\n\
4042 - **Today's Tasks**: {} tasks\n\
4043 - **Total Projects**: {} projects\n\
4044 - **Areas**: {} areas\n\
4045 - **Completed Tasks**: {} tasks\n\
4046 - **Incomplete Tasks**: {} tasks\n\n\
4047 ## Analysis Framework\n\
4048 Please analyze productivity patterns focusing on:\n\n\
4049 ### 1. Task Completion Patterns\n\
4050 - Completion rates over the period\n\
4051 - Task types that are completed vs. delayed\n\
4052 - Time patterns in task completion\n\n\
4053 ### 2. Workload Distribution\n\
4054 - Balance between different areas/projects\n\
4055 - Task complexity distribution\n\
4056 - Deadline adherence patterns\n\n\
4057 ### 3. Time Management\n\
4058 - Task scheduling effectiveness\n\
4059 - Inbox vs. scheduled task completion\n\
4060 - Overdue task patterns\n\n\
4061 ### 4. Project Progress\n\
4062 - Project completion rates\n\
4063 - Project complexity vs. completion time\n\
4064 - Area-based productivity differences\n\n\
4065 ## Key Insights\n\
4066 Identify:\n\
4067 - Peak productivity times\n\
4068 - Most/least productive areas\n\
4069 - Common bottlenecks\n\
4070 - Success patterns\n\n\
4071 ## Recommendations\n\
4072 {}",
4073 time_period,
4074 time_period,
4075 focus_area,
4076 inbox_tasks.len(),
4077 today_tasks.len(),
4078 projects.len(),
4079 areas.len(),
4080 completed_tasks,
4081 incomplete_tasks,
4082 if include_recommendations {
4083 "Provide specific, actionable recommendations for:\n\
4084 - Improving task completion rates\n\
4085 - Better time management\n\
4086 - Workload balancing\n\
4087 - Process optimization\n\
4088 - Goal setting and tracking"
4089 } else {
4090 "Focus on analysis without recommendations"
4091 }
4092 );
4093
4094 Ok(GetPromptResult {
4095 content: vec![Content::Text { text: prompt_text }],
4096 is_error: false,
4097 })
4098 }
4099
4100 async fn handle_backup_strategy_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
4102 let data_volume = args
4103 .get("data_volume")
4104 .and_then(|v| v.as_str())
4105 .ok_or_else(|| McpError::missing_parameter("data_volume"))?;
4106 let frequency = args
4107 .get("frequency")
4108 .and_then(|v| v.as_str())
4109 .ok_or_else(|| McpError::missing_parameter("frequency"))?;
4110 let retention_period = args
4111 .get("retention_period")
4112 .and_then(|v| v.as_str())
4113 .unwrap_or("3_months");
4114 let storage_preference = args
4115 .get("storage_preference")
4116 .and_then(|v| v.as_str())
4117 .unwrap_or("hybrid");
4118
4119 let db = &self.db;
4121 let projects = db.get_projects(None).await.map_err(|e| {
4122 McpError::database_operation_failed("get_projects for backup_strategy", e)
4123 })?;
4124 let areas = db
4125 .get_areas()
4126 .await
4127 .map_err(|e| McpError::database_operation_failed("get_areas for backup_strategy", e))?;
4128 let _ = db;
4129
4130 let prompt_text = format!(
4131 "# Backup Strategy Recommendation\n\n\
4132 ## Requirements\n\
4133 - **Data Volume**: {}\n\
4134 - **Backup Frequency**: {}\n\
4135 - **Retention Period**: {}\n\
4136 - **Storage Preference**: {}\n\n\
4137 ## Current Data Context\n\
4138 - **Projects**: {} projects\n\
4139 - **Areas**: {} areas\n\
4140 - **Database Type**: SQLite (Things 3)\n\n\
4141 ## Backup Strategy Analysis\n\n\
4142 ### 1. Data Assessment\n\
4143 Analyze the current data volume and growth patterns:\n\
4144 - Database size estimation\n\
4145 - Growth rate projections\n\
4146 - Critical data identification\n\n\
4147 ### 2. Backup Frequency Optimization\n\
4148 For {} frequency backups:\n\
4149 - Optimal timing considerations\n\
4150 - Incremental vs. full backup strategy\n\
4151 - Performance impact analysis\n\n\
4152 ### 3. Storage Strategy\n\
4153 For {} storage preference:\n\
4154 - Local storage recommendations\n\
4155 - Cloud storage options\n\
4156 - Hybrid approach benefits\n\
4157 - Cost considerations\n\n\
4158 ### 4. Retention Policy\n\
4159 For {} retention period:\n\
4160 - Data lifecycle management\n\
4161 - Compliance considerations\n\
4162 - Storage optimization\n\n\
4163 ## Recommended Implementation\n\
4164 Provide specific recommendations for:\n\
4165 - Backup tools and software\n\
4166 - Storage locations and providers\n\
4167 - Automation setup\n\
4168 - Monitoring and alerting\n\
4169 - Recovery procedures\n\n\
4170 ## Risk Mitigation\n\
4171 Address:\n\
4172 - Data loss prevention\n\
4173 - Backup verification\n\
4174 - Disaster recovery planning\n\
4175 - Security considerations\n\n\
4176 ## Cost Analysis\n\
4177 Estimate costs for:\n\
4178 - Storage requirements\n\
4179 - Backup software/tools\n\
4180 - Cloud services\n\
4181 - Maintenance overhead",
4182 data_volume,
4183 frequency,
4184 retention_period,
4185 storage_preference,
4186 projects.len(),
4187 areas.len(),
4188 frequency,
4189 storage_preference,
4190 retention_period
4191 );
4192
4193 Ok(GetPromptResult {
4194 content: vec![Content::Text { text: prompt_text }],
4195 is_error: false,
4196 })
4197 }
4198
4199 fn get_available_resources() -> Vec<Resource> {
4201 vec![
4202 Resource {
4203 uri: "things://inbox".to_string(),
4204 name: "Inbox Tasks".to_string(),
4205 description: "Current inbox tasks from Things 3".to_string(),
4206 mime_type: Some("application/json".to_string()),
4207 },
4208 Resource {
4209 uri: "things://projects".to_string(),
4210 name: "All Projects".to_string(),
4211 description: "All projects in Things 3".to_string(),
4212 mime_type: Some("application/json".to_string()),
4213 },
4214 Resource {
4215 uri: "things://areas".to_string(),
4216 name: "All Areas".to_string(),
4217 description: "All areas in Things 3".to_string(),
4218 mime_type: Some("application/json".to_string()),
4219 },
4220 Resource {
4221 uri: "things://today".to_string(),
4222 name: "Today's Tasks".to_string(),
4223 description: "Tasks scheduled for today".to_string(),
4224 mime_type: Some("application/json".to_string()),
4225 },
4226 ]
4227 }
4228
4229 async fn handle_resource_read(
4231 &self,
4232 request: ReadResourceRequest,
4233 ) -> McpResult<ReadResourceResult> {
4234 let uri = &request.uri;
4235
4236 let db = &self.db;
4237 let data = match uri.as_str() {
4238 "things://inbox" => {
4239 let tasks = db.get_inbox(None).await.map_err(|e| {
4240 McpError::database_operation_failed("get_inbox for resource", e)
4241 })?;
4242 serde_json::to_string_pretty(&tasks).map_err(|e| {
4243 McpError::serialization_failed("inbox resource serialization", e)
4244 })?
4245 }
4246 "things://projects" => {
4247 let projects = db.get_projects(None).await.map_err(|e| {
4248 McpError::database_operation_failed("get_projects for resource", e)
4249 })?;
4250 serde_json::to_string_pretty(&projects).map_err(|e| {
4251 McpError::serialization_failed("projects resource serialization", e)
4252 })?
4253 }
4254 "things://areas" => {
4255 let areas = db.get_areas().await.map_err(|e| {
4256 McpError::database_operation_failed("get_areas for resource", e)
4257 })?;
4258 serde_json::to_string_pretty(&areas).map_err(|e| {
4259 McpError::serialization_failed("areas resource serialization", e)
4260 })?
4261 }
4262 "things://today" => {
4263 let tasks = db.get_today(None).await.map_err(|e| {
4264 McpError::database_operation_failed("get_today for resource", e)
4265 })?;
4266 let _ = db;
4267 serde_json::to_string_pretty(&tasks).map_err(|e| {
4268 McpError::serialization_failed("today resource serialization", e)
4269 })?
4270 }
4271 _ => {
4272 return Err(McpError::resource_not_found(uri));
4273 }
4274 };
4275
4276 Ok(ReadResourceResult {
4277 contents: vec![Content::Text { text: data }],
4278 })
4279 }
4280
4281 pub async fn handle_jsonrpc_request(
4288 &self,
4289 request: serde_json::Value,
4290 ) -> things3_core::Result<Option<serde_json::Value>> {
4291 use serde_json::json;
4292
4293 let method = request["method"].as_str().ok_or_else(|| {
4294 things3_core::ThingsError::unknown("Missing method in JSON-RPC request".to_string())
4295 })?;
4296 let params = request["params"].clone();
4297
4298 let is_notification = request.get("id").is_none();
4301
4302 if is_notification {
4304 match method {
4305 "notifications/initialized" => {
4306 return Ok(None);
4308 }
4309 _ => {
4310 return Ok(None);
4312 }
4313 }
4314 }
4315
4316 let id = request["id"].clone();
4318
4319 let result = match method {
4320 "initialize" => {
4321 json!({
4323 "protocolVersion": "2024-11-05",
4324 "capabilities": {
4325 "tools": { "listChanged": false },
4326 "resources": { "subscribe": false, "listChanged": false },
4327 "prompts": { "listChanged": false }
4328 },
4329 "serverInfo": {
4330 "name": "things3-mcp",
4331 "version": env!("CARGO_PKG_VERSION")
4332 }
4333 })
4334 }
4335 "tools/list" => {
4336 let tools_result = self.list_tools().map_err(|e| {
4337 things3_core::ThingsError::unknown(format!("Failed to list tools: {}", e))
4338 })?;
4339 json!(tools_result.tools)
4340 }
4341 "tools/call" => {
4342 let tool_name = params["name"]
4343 .as_str()
4344 .ok_or_else(|| {
4345 things3_core::ThingsError::unknown(
4346 "Missing tool name in params".to_string(),
4347 )
4348 })?
4349 .to_string();
4350 let arguments = params["arguments"].clone();
4351
4352 let call_request = CallToolRequest {
4353 name: tool_name,
4354 arguments: Some(arguments),
4355 };
4356
4357 let call_result = self.call_tool(call_request).await.map_err(|e| {
4358 things3_core::ThingsError::unknown(format!("Failed to call tool: {}", e))
4359 })?;
4360
4361 json!(call_result)
4362 }
4363 "resources/list" => {
4364 let resources_result = self.list_resources().map_err(|e| {
4365 things3_core::ThingsError::unknown(format!("Failed to list resources: {}", e))
4366 })?;
4367 json!(resources_result.resources)
4368 }
4369 "resources/read" => {
4370 let uri = params["uri"]
4371 .as_str()
4372 .ok_or_else(|| {
4373 things3_core::ThingsError::unknown("Missing URI in params".to_string())
4374 })?
4375 .to_string();
4376
4377 let read_request = ReadResourceRequest { uri };
4378 let read_result = self.read_resource(read_request).await.map_err(|e| {
4379 things3_core::ThingsError::unknown(format!("Failed to read resource: {}", e))
4380 })?;
4381
4382 json!(read_result)
4383 }
4384 "prompts/list" => {
4385 let prompts_result = self.list_prompts().map_err(|e| {
4386 things3_core::ThingsError::unknown(format!("Failed to list prompts: {}", e))
4387 })?;
4388 json!(prompts_result.prompts)
4389 }
4390 "prompts/get" => {
4391 let prompt_name = params["name"]
4392 .as_str()
4393 .ok_or_else(|| {
4394 things3_core::ThingsError::unknown(
4395 "Missing prompt name in params".to_string(),
4396 )
4397 })?
4398 .to_string();
4399 let arguments = params.get("arguments").cloned();
4400
4401 let get_request = GetPromptRequest {
4402 name: prompt_name,
4403 arguments,
4404 };
4405
4406 let get_result = self.get_prompt(get_request).await.map_err(|e| {
4407 things3_core::ThingsError::unknown(format!("Failed to get prompt: {}", e))
4408 })?;
4409
4410 json!(get_result)
4411 }
4412 _ => {
4413 return Ok(Some(json!({
4414 "jsonrpc": "2.0",
4415 "id": id,
4416 "error": {
4417 "code": -32601,
4418 "message": format!("Method not found: {}", method)
4419 }
4420 })));
4421 }
4422 };
4423
4424 Ok(Some(json!({
4425 "jsonrpc": "2.0",
4426 "id": id,
4427 "result": result
4428 })))
4429 }
4430}