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