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