use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
use things3_core::{
BackupManager, DataExporter, DeleteChildHandling, McpServerConfig, PerformanceMonitor,
ThingsCache, ThingsConfig, ThingsDatabase, ThingsError,
};
use thiserror::Error;
use tokio::sync::Mutex;
use uuid::Uuid;
pub mod io_wrapper;
pub mod middleware;
pub mod test_harness;
use io_wrapper::{McpIo, StdIo};
use middleware::{MiddlewareChain, MiddlewareConfig};
#[derive(Error, Debug)]
pub enum McpError {
#[error("Tool not found: {tool_name}")]
ToolNotFound { tool_name: String },
#[error("Resource not found: {uri}")]
ResourceNotFound { uri: String },
#[error("Prompt not found: {prompt_name}")]
PromptNotFound { prompt_name: String },
#[error("Invalid parameter: {parameter_name} - {message}")]
InvalidParameter {
parameter_name: String,
message: String,
},
#[error("Missing required parameter: {parameter_name}")]
MissingParameter { parameter_name: String },
#[error("Invalid format: {format} - supported formats: {supported}")]
InvalidFormat { format: String, supported: String },
#[error("Invalid data type: {data_type} - supported types: {supported}")]
InvalidDataType {
data_type: String,
supported: String,
},
#[error("Database operation failed: {operation}")]
DatabaseOperationFailed {
operation: String,
source: ThingsError,
},
#[error("Backup operation failed: {operation}")]
BackupOperationFailed {
operation: String,
source: ThingsError,
},
#[error("Export operation failed: {operation}")]
ExportOperationFailed {
operation: String,
source: ThingsError,
},
#[error("Performance monitoring failed: {operation}")]
PerformanceMonitoringFailed {
operation: String,
source: ThingsError,
},
#[error("Cache operation failed: {operation}")]
CacheOperationFailed {
operation: String,
source: ThingsError,
},
#[error("Serialization failed: {operation}")]
SerializationFailed {
operation: String,
source: serde_json::Error,
},
#[error("IO operation failed: {operation}")]
IoOperationFailed {
operation: String,
source: std::io::Error,
},
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
#[error("Validation error: {message}")]
ValidationError { message: String },
#[error("Internal error: {message}")]
InternalError { message: String },
}
impl McpError {
pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
Self::ToolNotFound {
tool_name: tool_name.into(),
}
}
pub fn resource_not_found(uri: impl Into<String>) -> Self {
Self::ResourceNotFound { uri: uri.into() }
}
pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
Self::PromptNotFound {
prompt_name: prompt_name.into(),
}
}
pub fn invalid_parameter(
parameter_name: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::InvalidParameter {
parameter_name: parameter_name.into(),
message: message.into(),
}
}
pub fn missing_parameter(parameter_name: impl Into<String>) -> Self {
Self::MissingParameter {
parameter_name: parameter_name.into(),
}
}
pub fn invalid_format(format: impl Into<String>, supported: impl Into<String>) -> Self {
Self::InvalidFormat {
format: format.into(),
supported: supported.into(),
}
}
pub fn invalid_data_type(data_type: impl Into<String>, supported: impl Into<String>) -> Self {
Self::InvalidDataType {
data_type: data_type.into(),
supported: supported.into(),
}
}
pub fn database_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
Self::DatabaseOperationFailed {
operation: operation.into(),
source,
}
}
pub fn backup_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
Self::BackupOperationFailed {
operation: operation.into(),
source,
}
}
pub fn export_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
Self::ExportOperationFailed {
operation: operation.into(),
source,
}
}
pub fn performance_monitoring_failed(
operation: impl Into<String>,
source: ThingsError,
) -> Self {
Self::PerformanceMonitoringFailed {
operation: operation.into(),
source,
}
}
pub fn cache_operation_failed(operation: impl Into<String>, source: ThingsError) -> Self {
Self::CacheOperationFailed {
operation: operation.into(),
source,
}
}
pub fn serialization_failed(operation: impl Into<String>, source: serde_json::Error) -> Self {
Self::SerializationFailed {
operation: operation.into(),
source,
}
}
pub fn io_operation_failed(operation: impl Into<String>, source: std::io::Error) -> Self {
Self::IoOperationFailed {
operation: operation.into(),
source,
}
}
pub fn configuration_error(message: impl Into<String>) -> Self {
Self::ConfigurationError {
message: message.into(),
}
}
pub fn validation_error(message: impl Into<String>) -> Self {
Self::ValidationError {
message: message.into(),
}
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::InternalError {
message: message.into(),
}
}
#[must_use]
pub fn to_call_result(self) -> CallToolResult {
let error_message = match &self {
McpError::ToolNotFound { tool_name } => {
format!("Tool '{tool_name}' not found. Available tools can be listed using the list_tools method.")
}
McpError::ResourceNotFound { uri } => {
format!("Resource '{uri}' not found. Available resources can be listed using the list_resources method.")
}
McpError::PromptNotFound { prompt_name } => {
format!("Prompt '{prompt_name}' not found. Available prompts can be listed using the list_prompts method.")
}
McpError::InvalidParameter {
parameter_name,
message,
} => {
format!("Invalid parameter '{parameter_name}': {message}. Please check the parameter format and try again.")
}
McpError::MissingParameter { parameter_name } => {
format!("Missing required parameter '{parameter_name}'. Please provide this parameter and try again.")
}
McpError::InvalidFormat { format, supported } => {
format!("Invalid format '{format}'. Supported formats: {supported}. Please use one of the supported formats.")
}
McpError::InvalidDataType {
data_type,
supported,
} => {
format!("Invalid data type '{data_type}'. Supported types: {supported}. Please use one of the supported types.")
}
McpError::DatabaseOperationFailed { operation, source } => {
format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
}
McpError::BackupOperationFailed { operation, source } => {
format!("Backup operation '{operation}' failed: {source}. Please check backup permissions and try again.")
}
McpError::ExportOperationFailed { operation, source } => {
format!("Export operation '{operation}' failed: {source}. Please check export parameters and try again.")
}
McpError::PerformanceMonitoringFailed { operation, source } => {
format!("Performance monitoring '{operation}' failed: {source}. Please try again later.")
}
McpError::CacheOperationFailed { operation, source } => {
format!("Cache operation '{operation}' failed: {source}. Please try again later.")
}
McpError::SerializationFailed { operation, source } => {
format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
}
McpError::IoOperationFailed { operation, source } => {
format!("IO operation '{operation}' failed: {source}. Please check file permissions and try again.")
}
McpError::ConfigurationError { message } => {
format!("Configuration error: {message}. Please check your configuration and try again.")
}
McpError::ValidationError { message } => {
format!("Validation error: {message}. Please check your input and try again.")
}
McpError::InternalError { message } => {
format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
}
};
CallToolResult {
content: vec![Content::Text {
text: error_message,
}],
is_error: true,
}
}
#[must_use]
pub fn to_prompt_result(self) -> GetPromptResult {
let error_message = match &self {
McpError::PromptNotFound { prompt_name } => {
format!("Prompt '{prompt_name}' not found. Available prompts can be listed using the list_prompts method.")
}
McpError::InvalidParameter {
parameter_name,
message,
} => {
format!("Invalid parameter '{parameter_name}': {message}. Please check the parameter format and try again.")
}
McpError::MissingParameter { parameter_name } => {
format!("Missing required parameter '{parameter_name}'. Please provide this parameter and try again.")
}
McpError::DatabaseOperationFailed { operation, source } => {
format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
}
McpError::SerializationFailed { operation, source } => {
format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
}
McpError::ValidationError { message } => {
format!("Validation error: {message}. Please check your input and try again.")
}
McpError::InternalError { message } => {
format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
}
_ => {
format!("Error: {self}. Please try again later.")
}
};
GetPromptResult {
content: vec![Content::Text {
text: error_message,
}],
is_error: true,
}
}
#[must_use]
pub fn to_resource_result(self) -> ReadResourceResult {
let error_message = match &self {
McpError::ResourceNotFound { uri } => {
format!("Resource '{uri}' not found. Available resources can be listed using the list_resources method.")
}
McpError::DatabaseOperationFailed { operation, source } => {
format!("Database operation '{operation}' failed: {source}. Please check your database connection and try again.")
}
McpError::SerializationFailed { operation, source } => {
format!("Serialization '{operation}' failed: {source}. Please check data format and try again.")
}
McpError::InternalError { message } => {
format!("Internal error: {message}. Please try again later or contact support if the issue persists.")
}
_ => {
format!("Error: {self}. Please try again later.")
}
};
ReadResourceResult {
contents: vec![Content::Text {
text: error_message,
}],
}
}
}
pub type McpResult<T> = std::result::Result<T, McpError>;
impl From<ThingsError> for McpError {
fn from(error: ThingsError) -> Self {
match error {
ThingsError::Database(e) => {
McpError::database_operation_failed("database operation", ThingsError::Database(e))
}
ThingsError::Serialization(e) => McpError::serialization_failed("serialization", e),
ThingsError::Io(e) => McpError::io_operation_failed("io operation", e),
ThingsError::DatabaseNotFound { path } => {
McpError::configuration_error(format!("Database not found at: {path}"))
}
ThingsError::InvalidUuid { uuid } => {
McpError::validation_error(format!("Invalid UUID format: {uuid}"))
}
ThingsError::InvalidDate { date } => {
McpError::validation_error(format!("Invalid date format: {date}"))
}
ThingsError::TaskNotFound { uuid } => {
McpError::validation_error(format!("Task not found: {uuid}"))
}
ThingsError::ProjectNotFound { uuid } => {
McpError::validation_error(format!("Project not found: {uuid}"))
}
ThingsError::AreaNotFound { uuid } => {
McpError::validation_error(format!("Area not found: {uuid}"))
}
ThingsError::Validation { message } => McpError::validation_error(message),
ThingsError::Configuration { message } => McpError::configuration_error(message),
ThingsError::DateValidation(e) => {
McpError::validation_error(format!("Date validation failed: {e}"))
}
ThingsError::DateConversion(e) => {
McpError::validation_error(format!("Date conversion failed: {e}"))
}
ThingsError::Unknown { message } => McpError::internal_error(message),
}
}
}
impl From<serde_json::Error> for McpError {
fn from(error: serde_json::Error) -> Self {
McpError::serialization_failed("json serialization", error)
}
}
impl From<std::io::Error> for McpError {
fn from(error: std::io::Error) -> Self {
McpError::io_operation_failed("file operation", error)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolRequest {
pub name: String,
pub arguments: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallToolResult {
pub content: Vec<Content>,
pub is_error: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Content {
Text { text: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Resource {
pub uri: String,
pub name: String,
pub description: String,
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListResourcesResult {
pub resources: Vec<Resource>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReadResourceRequest {
pub uri: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<Content>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Prompt {
pub name: String,
pub description: String,
pub arguments: Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListPromptsResult {
pub prompts: Vec<Prompt>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetPromptRequest {
pub name: String,
pub arguments: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetPromptResult {
pub content: Vec<Content>,
pub is_error: bool,
}
pub struct ThingsMcpServer {
#[allow(dead_code)]
pub db: Arc<ThingsDatabase>,
#[allow(dead_code)]
cache: Arc<Mutex<ThingsCache>>,
#[allow(dead_code)]
performance_monitor: Arc<Mutex<PerformanceMonitor>>,
#[allow(dead_code)]
exporter: DataExporter,
#[allow(dead_code)]
backup_manager: Arc<Mutex<BackupManager>>,
middleware_chain: MiddlewareChain,
}
#[allow(dead_code)]
pub async fn start_mcp_server(
db: Arc<ThingsDatabase>,
config: ThingsConfig,
) -> things3_core::Result<()> {
let io = StdIo::new();
start_mcp_server_generic(db, config, io).await
}
pub async fn start_mcp_server_generic<I: McpIo>(
db: Arc<ThingsDatabase>,
config: ThingsConfig,
mut io: I,
) -> things3_core::Result<()> {
let server = Arc::new(tokio::sync::Mutex::new(ThingsMcpServer::new(db, config)));
loop {
let line = io.read_line().await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to read from input: {}", e))
})?;
let Some(line) = line else {
break;
};
if line.is_empty() {
continue;
}
let request: serde_json::Value = serde_json::from_str(&line).map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to parse JSON-RPC request: {}", e))
})?;
let server_clone = Arc::clone(&server);
let response_opt = {
let server = server_clone.lock().await;
server.handle_jsonrpc_request(request).await
}?;
if let Some(response) = response_opt {
let response_str = serde_json::to_string(&response).map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to serialize response: {}", e))
})?;
io.write_line(&response_str).await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to write response: {}", e))
})?;
io.flush().await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to flush output: {}", e))
})?;
}
}
Ok(())
}
pub async fn start_mcp_server_with_config(
db: Arc<ThingsDatabase>,
mcp_config: McpServerConfig,
) -> things3_core::Result<()> {
let io = StdIo::new();
start_mcp_server_with_config_generic(db, mcp_config, io).await
}
pub async fn start_mcp_server_with_config_generic<I: McpIo>(
db: Arc<ThingsDatabase>,
mcp_config: McpServerConfig,
mut io: I,
) -> things3_core::Result<()> {
let things_config = ThingsConfig::new(
mcp_config.database.path.clone(),
mcp_config.database.fallback_to_default,
);
let server = Arc::new(tokio::sync::Mutex::new(
ThingsMcpServer::new_with_mcp_config(db, things_config, mcp_config),
));
loop {
let line = io.read_line().await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to read from input: {}", e))
})?;
let Some(line) = line else {
break;
};
if line.is_empty() {
continue;
}
let request: serde_json::Value = serde_json::from_str(&line).map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to parse JSON-RPC request: {}", e))
})?;
let server_clone = Arc::clone(&server);
let response_opt = {
let server = server_clone.lock().await;
server.handle_jsonrpc_request(request).await
}?;
if let Some(response) = response_opt {
let response_str = serde_json::to_string(&response).map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to serialize response: {}", e))
})?;
io.write_line(&response_str).await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to write response: {}", e))
})?;
io.flush().await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to flush output: {}", e))
})?;
}
}
Ok(())
}
impl ThingsMcpServer {
#[must_use]
pub fn new(db: Arc<ThingsDatabase>, config: ThingsConfig) -> Self {
let cache = ThingsCache::new_default();
let performance_monitor = PerformanceMonitor::new_default();
let exporter = DataExporter::new_default();
let backup_manager = BackupManager::new(config);
let mut middleware_config = MiddlewareConfig::default();
middleware_config.logging.enabled = false; let middleware_chain = middleware_config.build_chain();
Self {
db,
cache: Arc::new(Mutex::new(cache)),
performance_monitor: Arc::new(Mutex::new(performance_monitor)),
exporter,
backup_manager: Arc::new(Mutex::new(backup_manager)),
middleware_chain,
}
}
#[must_use]
pub fn with_middleware_config(
db: ThingsDatabase,
config: ThingsConfig,
middleware_config: MiddlewareConfig,
) -> Self {
let cache = ThingsCache::new_default();
let performance_monitor = PerformanceMonitor::new_default();
let exporter = DataExporter::new_default();
let backup_manager = BackupManager::new(config);
let middleware_chain = middleware_config.build_chain();
Self {
db: Arc::new(db),
cache: Arc::new(Mutex::new(cache)),
performance_monitor: Arc::new(Mutex::new(performance_monitor)),
exporter,
backup_manager: Arc::new(Mutex::new(backup_manager)),
middleware_chain,
}
}
#[must_use]
pub fn new_with_mcp_config(
db: Arc<ThingsDatabase>,
config: ThingsConfig,
mcp_config: McpServerConfig,
) -> Self {
let cache = ThingsCache::new_default();
let performance_monitor = PerformanceMonitor::new_default();
let exporter = DataExporter::new_default();
let backup_manager = BackupManager::new(config);
let middleware_config = MiddlewareConfig {
logging: middleware::LoggingConfig {
enabled: false, level: mcp_config.logging.level.clone(),
},
validation: middleware::ValidationConfig {
enabled: mcp_config.security.validation.enabled,
strict_mode: mcp_config.security.validation.strict_mode,
},
performance: middleware::PerformanceConfig {
enabled: mcp_config.performance.enabled,
slow_request_threshold_ms: mcp_config.performance.slow_request_threshold_ms,
},
security: middleware::SecurityConfig {
authentication: middleware::AuthenticationConfig {
enabled: mcp_config.security.authentication.enabled,
require_auth: mcp_config.security.authentication.require_auth,
jwt_secret: mcp_config.security.authentication.jwt_secret,
api_keys: mcp_config
.security
.authentication
.api_keys
.iter()
.map(|key| middleware::ApiKeyConfig {
key: key.key.clone(),
key_id: key.key_id.clone(),
permissions: key.permissions.clone(),
expires_at: key.expires_at.clone(),
})
.collect(),
oauth: mcp_config
.security
.authentication
.oauth
.as_ref()
.map(|oauth| middleware::OAuth2Config {
client_id: oauth.client_id.clone(),
client_secret: oauth.client_secret.clone(),
token_endpoint: oauth.token_endpoint.clone(),
scopes: oauth.scopes.clone(),
}),
},
rate_limiting: middleware::RateLimitingConfig {
enabled: mcp_config.security.rate_limiting.enabled,
requests_per_minute: mcp_config.security.rate_limiting.requests_per_minute,
burst_limit: mcp_config.security.rate_limiting.burst_limit,
custom_limits: mcp_config.security.rate_limiting.custom_limits.clone(),
},
},
};
let middleware_chain = middleware_config.build_chain();
Self {
db,
cache: Arc::new(Mutex::new(cache)),
performance_monitor: Arc::new(Mutex::new(performance_monitor)),
exporter,
backup_manager: Arc::new(Mutex::new(backup_manager)),
middleware_chain,
}
}
#[must_use]
pub fn middleware_chain(&self) -> &MiddlewareChain {
&self.middleware_chain
}
pub fn list_tools(&self) -> McpResult<ListToolsResult> {
Ok(ListToolsResult {
tools: Self::get_available_tools(),
})
}
pub async fn call_tool(&self, request: CallToolRequest) -> McpResult<CallToolResult> {
self.middleware_chain
.execute(
request,
|req| async move { self.handle_tool_call(req).await },
)
.await
}
pub async fn call_tool_with_fallback(&self, request: CallToolRequest) -> CallToolResult {
match self.handle_tool_call(request).await {
Ok(result) => result,
Err(error) => error.to_call_result(),
}
}
pub fn list_resources(&self) -> McpResult<ListResourcesResult> {
Ok(ListResourcesResult {
resources: Self::get_available_resources(),
})
}
pub async fn read_resource(
&self,
request: ReadResourceRequest,
) -> McpResult<ReadResourceResult> {
self.handle_resource_read(request).await
}
pub async fn read_resource_with_fallback(
&self,
request: ReadResourceRequest,
) -> ReadResourceResult {
match self.handle_resource_read(request).await {
Ok(result) => result,
Err(error) => error.to_resource_result(),
}
}
pub fn list_prompts(&self) -> McpResult<ListPromptsResult> {
Ok(ListPromptsResult {
prompts: Self::get_available_prompts(),
})
}
pub async fn get_prompt(&self, request: GetPromptRequest) -> McpResult<GetPromptResult> {
self.handle_prompt_request(request).await
}
pub async fn get_prompt_with_fallback(&self, request: GetPromptRequest) -> GetPromptResult {
match self.handle_prompt_request(request).await {
Ok(result) => result,
Err(error) => error.to_prompt_result(),
}
}
fn get_available_tools() -> Vec<Tool> {
let mut tools = Vec::new();
tools.extend(Self::get_data_retrieval_tools());
tools.extend(Self::get_task_management_tools());
tools.extend(Self::get_bulk_operation_tools());
tools.extend(Self::get_tag_management_tools());
tools.extend(Self::get_analytics_tools());
tools.extend(Self::get_backup_tools());
tools.extend(Self::get_system_tools());
tools
}
fn get_data_retrieval_tools() -> Vec<Tool> {
vec![
Tool {
name: "get_inbox".to_string(),
description: "Get tasks from the inbox".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tasks to return"
}
}
}),
},
Tool {
name: "get_today".to_string(),
description: "Get tasks scheduled for today".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tasks to return"
}
}
}),
},
Tool {
name: "get_projects".to_string(),
description: "Get all projects, optionally filtered by area".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"area_uuid": {
"type": "string",
"description": "Optional area UUID to filter projects"
}
}
}),
},
Tool {
name: "get_areas".to_string(),
description: "Get all areas".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
Tool {
name: "search_tasks".to_string(),
description: "Search for tasks by query".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "integer",
"description": "Maximum number of tasks to return"
}
},
"required": ["query"]
}),
},
Tool {
name: "get_recent_tasks".to_string(),
description: "Get recently created or modified tasks".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tasks to return"
},
"hours": {
"type": "integer",
"description": "Number of hours to look back"
}
}
}),
},
Tool {
name: "logbook_search".to_string(),
description: "Search completed tasks in the Things 3 logbook. Supports text search, date ranges, and filtering by project/area/tags.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"search_text": {
"type": "string",
"description": "Search in task titles and notes (case-insensitive)"
},
"from_date": {
"type": "string",
"format": "date",
"description": "Start date for completion date range (YYYY-MM-DD)"
},
"to_date": {
"type": "string",
"format": "date",
"description": "End date for completion date range (YYYY-MM-DD)"
},
"project_uuid": {
"type": "string",
"format": "uuid",
"description": "Filter by project UUID"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "Filter by area UUID"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Filter by one or more tags (all must match)"
},
"limit": {
"type": "integer",
"default": 50,
"minimum": 1,
"maximum": 500,
"description": "Maximum number of results to return (default: 50, max: 500)"
}
}
}),
},
]
}
fn get_task_management_tools() -> Vec<Tool> {
vec![
Tool {
name: "create_task".to_string(),
description: "Create a new task in Things 3".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Task title (required)"
},
"task_type": {
"type": "string",
"enum": ["to-do", "project", "heading"],
"description": "Task type (default: to-do)"
},
"notes": {
"type": "string",
"description": "Task notes"
},
"start_date": {
"type": "string",
"format": "date",
"description": "Start date (YYYY-MM-DD)"
},
"deadline": {
"type": "string",
"format": "date",
"description": "Deadline (YYYY-MM-DD)"
},
"project_uuid": {
"type": "string",
"format": "uuid",
"description": "Project UUID"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "Area UUID"
},
"parent_uuid": {
"type": "string",
"format": "uuid",
"description": "Parent task UUID (for subtasks)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tag names"
},
"status": {
"type": "string",
"enum": ["incomplete", "completed", "canceled", "trashed"],
"description": "Initial status (default: incomplete)"
}
},
"required": ["title"]
}),
},
Tool {
name: "update_task".to_string(),
description: "Update an existing task (only provided fields will be updated)"
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Task UUID (required)"
},
"title": {
"type": "string",
"description": "New task title"
},
"notes": {
"type": "string",
"description": "New task notes"
},
"start_date": {
"type": "string",
"format": "date",
"description": "New start date (YYYY-MM-DD)"
},
"deadline": {
"type": "string",
"format": "date",
"description": "New deadline (YYYY-MM-DD)"
},
"status": {
"type": "string",
"enum": ["incomplete", "completed", "canceled", "trashed"],
"description": "New task status"
},
"project_uuid": {
"type": "string",
"format": "uuid",
"description": "New project UUID"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "New area UUID"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "New tag names"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "complete_task".to_string(),
description: "Mark a task as completed".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the task to complete"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "uncomplete_task".to_string(),
description: "Mark a completed task as incomplete".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the task to mark incomplete"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "delete_task".to_string(),
description: "Soft delete a task (set trashed=1)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the task to delete"
},
"child_handling": {
"type": "string",
"enum": ["error", "cascade", "orphan"],
"default": "error",
"description": "How to handle child tasks: error (fail if children exist), cascade (delete children too), orphan (delete parent only)"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "bulk_create_tasks".to_string(),
description: "Create multiple tasks at once".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"tasks": {
"type": "array",
"description": "Array of task objects to create",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"notes": {"type": "string"},
"project_uuid": {"type": "string"},
"area_uuid": {"type": "string"}
},
"required": ["title"]
}
}
},
"required": ["tasks"]
}),
},
Tool {
name: "create_project".to_string(),
description: "Create a new project (a task with type=project)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Project title (required)"
},
"notes": {
"type": "string",
"description": "Project notes"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "Area UUID"
},
"start_date": {
"type": "string",
"format": "date",
"description": "Start date (YYYY-MM-DD)"
},
"deadline": {
"type": "string",
"format": "date",
"description": "Deadline (YYYY-MM-DD)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tag names"
}
},
"required": ["title"]
}),
},
Tool {
name: "update_project".to_string(),
description: "Update an existing project (only provided fields will be updated)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Project UUID (required)"
},
"title": {
"type": "string",
"description": "New project title"
},
"notes": {
"type": "string",
"description": "New project notes"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "New area UUID"
},
"start_date": {
"type": "string",
"format": "date",
"description": "New start date (YYYY-MM-DD)"
},
"deadline": {
"type": "string",
"format": "date",
"description": "New deadline (YYYY-MM-DD)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "New tag names"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "complete_project".to_string(),
description: "Mark a project as completed, with options for handling child tasks".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the project to complete"
},
"child_handling": {
"type": "string",
"enum": ["error", "cascade", "orphan"],
"default": "error",
"description": "How to handle child tasks: error (fail if children exist), cascade (complete children too), orphan (move children to inbox)"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "delete_project".to_string(),
description: "Soft delete a project (set trashed=1), with options for handling child tasks".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the project to delete"
},
"child_handling": {
"type": "string",
"enum": ["error", "cascade", "orphan"],
"default": "error",
"description": "How to handle child tasks: error (fail if children exist), cascade (delete children too), orphan (move children to inbox)"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "create_area".to_string(),
description: "Create a new area".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Area title (required)"
}
},
"required": ["title"]
}),
},
Tool {
name: "update_area".to_string(),
description: "Update an existing area".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Area UUID (required)"
},
"title": {
"type": "string",
"description": "New area title (required)"
}
},
"required": ["uuid", "title"]
}),
},
Tool {
name: "delete_area".to_string(),
description: "Delete an area (hard delete). All projects in this area will be moved to no area.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of the area to delete"
}
},
"required": ["uuid"]
}),
},
]
}
fn get_analytics_tools() -> Vec<Tool> {
vec![
Tool {
name: "get_productivity_metrics".to_string(),
description: "Get productivity metrics and statistics".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"days": {
"type": "integer",
"description": "Number of days to look back for metrics"
}
}
}),
},
Tool {
name: "export_data".to_string(),
description: "Export data in various formats".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "Export format",
"enum": ["json", "csv", "markdown"]
},
"data_type": {
"type": "string",
"description": "Type of data to export",
"enum": ["tasks", "projects", "areas", "all"]
}
},
"required": ["format", "data_type"]
}),
},
]
}
fn get_backup_tools() -> Vec<Tool> {
vec![
Tool {
name: "backup_database".to_string(),
description: "Create a backup of the Things 3 database".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"backup_dir": {
"type": "string",
"description": "Directory to store the backup"
},
"description": {
"type": "string",
"description": "Optional description for the backup"
}
},
"required": ["backup_dir"]
}),
},
Tool {
name: "restore_database".to_string(),
description: "Restore from a backup".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"backup_path": {
"type": "string",
"description": "Path to the backup file"
}
},
"required": ["backup_path"]
}),
},
Tool {
name: "list_backups".to_string(),
description: "List available backups".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"backup_dir": {
"type": "string",
"description": "Directory containing backups"
}
},
"required": ["backup_dir"]
}),
},
]
}
fn get_system_tools() -> Vec<Tool> {
vec![
Tool {
name: "get_performance_stats".to_string(),
description: "Get performance statistics and metrics".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
Tool {
name: "get_system_metrics".to_string(),
description: "Get current system resource metrics".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
Tool {
name: "get_cache_stats".to_string(),
description: "Get cache statistics and hit rates".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
]
}
fn get_bulk_operation_tools() -> Vec<Tool> {
vec![
Tool {
name: "bulk_move".to_string(),
description: "Move multiple tasks to a project or area (transactional)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of task UUIDs to move"
},
"project_uuid": {
"type": "string",
"format": "uuid",
"description": "Target project UUID (optional)"
},
"area_uuid": {
"type": "string",
"format": "uuid",
"description": "Target area UUID (optional)"
}
},
"required": ["task_uuids"]
}),
},
Tool {
name: "bulk_update_dates".to_string(),
description: "Update dates for multiple tasks with validation (transactional)"
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of task UUIDs to update"
},
"start_date": {
"type": "string",
"format": "date",
"description": "New start date (YYYY-MM-DD, optional)"
},
"deadline": {
"type": "string",
"format": "date",
"description": "New deadline (YYYY-MM-DD, optional)"
},
"clear_start_date": {
"type": "boolean",
"description": "Clear start date (set to NULL, default: false)"
},
"clear_deadline": {
"type": "boolean",
"description": "Clear deadline (set to NULL, default: false)"
}
},
"required": ["task_uuids"]
}),
},
Tool {
name: "bulk_complete".to_string(),
description: "Mark multiple tasks as completed (transactional)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of task UUIDs to complete"
}
},
"required": ["task_uuids"]
}),
},
Tool {
name: "bulk_delete".to_string(),
description: "Delete multiple tasks (soft delete, transactional)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of task UUIDs to delete"
}
},
"required": ["task_uuids"]
}),
},
]
}
fn get_tag_management_tools() -> Vec<Tool> {
vec![
Tool {
name: "search_tags".to_string(),
description: "Search for existing tags (finds exact and similar matches)"
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for tag titles"
},
"include_similar": {
"type": "boolean",
"description": "Include fuzzy matches (default: true)"
},
"min_similarity": {
"type": "number",
"description": "Minimum similarity score 0.0-1.0 (default: 0.7)"
}
},
"required": ["query"]
}),
},
Tool {
name: "get_tag_suggestions".to_string(),
description: "Get tag suggestions for a title (prevents duplicates)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Proposed tag title"
}
},
"required": ["title"]
}),
},
Tool {
name: "get_popular_tags".to_string(),
description: "Get most frequently used tags".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tags to return (default: 20)"
}
}
}),
},
Tool {
name: "get_recent_tags".to_string(),
description: "Get recently used tags".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tags to return (default: 20)"
}
}
}),
},
Tool {
name: "create_tag".to_string(),
description: "Create a new tag (checks for duplicates first)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Tag title (required)"
},
"shortcut": {
"type": "string",
"description": "Keyboard shortcut"
},
"parent_uuid": {
"type": "string",
"format": "uuid",
"description": "Parent tag UUID for nesting"
},
"force": {
"type": "boolean",
"description": "Skip duplicate check (default: false)"
}
},
"required": ["title"]
}),
},
Tool {
name: "update_tag".to_string(),
description: "Update an existing tag".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Tag UUID (required)"
},
"title": {
"type": "string",
"description": "New title"
},
"shortcut": {
"type": "string",
"description": "New shortcut"
},
"parent_uuid": {
"type": "string",
"format": "uuid",
"description": "New parent UUID"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "delete_tag".to_string(),
description: "Delete a tag".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Tag UUID (required)"
},
"remove_from_tasks": {
"type": "boolean",
"description": "Remove tag from all tasks (default: false)"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "merge_tags".to_string(),
description: "Merge two tags (combine source into target)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"source_uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of tag to merge from (will be deleted)"
},
"target_uuid": {
"type": "string",
"format": "uuid",
"description": "UUID of tag to merge into (will remain)"
}
},
"required": ["source_uuid", "target_uuid"]
}),
},
Tool {
name: "add_tag_to_task".to_string(),
description: "Add a tag to a task (suggests existing tags)".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuid": {
"type": "string",
"format": "uuid",
"description": "Task UUID (required)"
},
"tag_title": {
"type": "string",
"description": "Tag title (required)"
}
},
"required": ["task_uuid", "tag_title"]
}),
},
Tool {
name: "remove_tag_from_task".to_string(),
description: "Remove a tag from a task".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuid": {
"type": "string",
"format": "uuid",
"description": "Task UUID (required)"
},
"tag_title": {
"type": "string",
"description": "Tag title (required)"
}
},
"required": ["task_uuid", "tag_title"]
}),
},
Tool {
name: "set_task_tags".to_string(),
description: "Replace all tags on a task".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"task_uuid": {
"type": "string",
"format": "uuid",
"description": "Task UUID (required)"
},
"tag_titles": {
"type": "array",
"items": {"type": "string"},
"description": "Array of tag titles"
}
},
"required": ["task_uuid", "tag_titles"]
}),
},
Tool {
name: "get_tag_statistics".to_string(),
description: "Get detailed statistics for a tag".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid",
"description": "Tag UUID (required)"
}
},
"required": ["uuid"]
}),
},
Tool {
name: "find_duplicate_tags".to_string(),
description: "Find duplicate or highly similar tags".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"min_similarity": {
"type": "number",
"description": "Minimum similarity score 0.0-1.0 (default: 0.85)"
}
}
}),
},
Tool {
name: "get_tag_completions".to_string(),
description: "Get tag auto-completions for partial input".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"partial_input": {
"type": "string",
"description": "Partial tag input (required)"
},
"limit": {
"type": "integer",
"description": "Maximum completions to return (default: 10)"
}
},
"required": ["partial_input"]
}),
},
]
}
async fn handle_tool_call(&self, request: CallToolRequest) -> McpResult<CallToolResult> {
let tool_name = &request.name;
let arguments = request.arguments.unwrap_or_default();
let result = match tool_name.as_str() {
"get_inbox" => self.handle_get_inbox(arguments).await,
"get_today" => self.handle_get_today(arguments).await,
"get_projects" => self.handle_get_projects(arguments).await,
"get_areas" => self.handle_get_areas(arguments).await,
"search_tasks" => self.handle_search_tasks(arguments).await,
"logbook_search" => self.handle_logbook_search(arguments).await,
"create_task" => self.handle_create_task(arguments).await,
"update_task" => self.handle_update_task(arguments).await,
"complete_task" => self.handle_complete_task(arguments).await,
"uncomplete_task" => self.handle_uncomplete_task(arguments).await,
"delete_task" => self.handle_delete_task(arguments).await,
"bulk_move" => self.handle_bulk_move(arguments).await,
"bulk_update_dates" => self.handle_bulk_update_dates(arguments).await,
"bulk_complete" => self.handle_bulk_complete(arguments).await,
"bulk_delete" => self.handle_bulk_delete(arguments).await,
"create_project" => self.handle_create_project(arguments).await,
"update_project" => self.handle_update_project(arguments).await,
"complete_project" => self.handle_complete_project(arguments).await,
"delete_project" => self.handle_delete_project(arguments).await,
"create_area" => self.handle_create_area(arguments).await,
"update_area" => self.handle_update_area(arguments).await,
"delete_area" => self.handle_delete_area(arguments).await,
"get_productivity_metrics" => self.handle_get_productivity_metrics(arguments).await,
"export_data" => self.handle_export_data(arguments).await,
"bulk_create_tasks" => Self::handle_bulk_create_tasks(&arguments),
"get_recent_tasks" => self.handle_get_recent_tasks(arguments).await,
"backup_database" => self.handle_backup_database(arguments).await,
"restore_database" => self.handle_restore_database(arguments).await,
"list_backups" => self.handle_list_backups(arguments).await,
"get_performance_stats" => self.handle_get_performance_stats(arguments).await,
"get_system_metrics" => self.handle_get_system_metrics(arguments).await,
"get_cache_stats" => self.handle_get_cache_stats(arguments).await,
"search_tags" => self.handle_search_tags_tool(arguments).await,
"get_tag_suggestions" => self.handle_get_tag_suggestions(arguments).await,
"get_popular_tags" => self.handle_get_popular_tags(arguments).await,
"get_recent_tags" => self.handle_get_recent_tags(arguments).await,
"create_tag" => self.handle_create_tag(arguments).await,
"update_tag" => self.handle_update_tag(arguments).await,
"delete_tag" => self.handle_delete_tag(arguments).await,
"merge_tags" => self.handle_merge_tags(arguments).await,
"add_tag_to_task" => self.handle_add_tag_to_task(arguments).await,
"remove_tag_from_task" => self.handle_remove_tag_from_task(arguments).await,
"set_task_tags" => self.handle_set_task_tags(arguments).await,
"get_tag_statistics" => self.handle_get_tag_statistics(arguments).await,
"find_duplicate_tags" => self.handle_find_duplicate_tags(arguments).await,
"get_tag_completions" => self.handle_get_tag_completions(arguments).await,
_ => {
return Err(McpError::tool_not_found(tool_name));
}
};
result
}
async fn handle_get_inbox(&self, args: Value) -> McpResult<CallToolResult> {
let limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX));
let tasks = self
.db
.get_inbox(limit)
.await
.map_err(|e| McpError::database_operation_failed("get_inbox", e))?;
let json = serde_json::to_string_pretty(&tasks)
.map_err(|e| McpError::serialization_failed("get_inbox serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_get_today(&self, args: Value) -> McpResult<CallToolResult> {
let limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX));
let tasks = self.db.get_today(limit).await.map_err(|e| {
McpError::database_operation_failed(
"get_today",
things3_core::ThingsError::unknown(format!("Failed to get today's tasks: {}", e)),
)
})?;
let json = serde_json::to_string_pretty(&tasks)
.map_err(|e| McpError::serialization_failed("get_today serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_get_projects(&self, args: Value) -> McpResult<CallToolResult> {
let _area_uuid = args
.get("area_uuid")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let projects = self
.db
.get_projects(None)
.await
.map_err(|e| McpError::database_operation_failed("get_projects", e))?;
let json = serde_json::to_string_pretty(&projects)
.map_err(|e| McpError::serialization_failed("get_projects serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_get_areas(&self, _args: Value) -> McpResult<CallToolResult> {
let areas = self
.db
.get_areas()
.await
.map_err(|e| McpError::database_operation_failed("get_areas", e))?;
let json = serde_json::to_string_pretty(&areas)
.map_err(|e| McpError::serialization_failed("get_areas serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_search_tasks(&self, args: Value) -> McpResult<CallToolResult> {
let query = args
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("query"))?;
let _limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX));
let tasks = self
.db
.search_tasks(query)
.await
.map_err(|e| McpError::database_operation_failed("search_tasks", e))?;
let json = serde_json::to_string_pretty(&tasks)
.map_err(|e| McpError::serialization_failed("search_tasks serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_logbook_search(&self, args: Value) -> McpResult<CallToolResult> {
let search_text = args
.get("search_text")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let from_date = args
.get("from_date")
.and_then(|v| v.as_str())
.and_then(|s| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
let to_date = args
.get("to_date")
.and_then(|v| v.as_str())
.and_then(|s| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
let project_uuid = args
.get("project_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok());
let area_uuid = args
.get("area_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok());
let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<String>>()
});
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as u32);
let tasks = self
.db
.search_logbook(
search_text,
from_date,
to_date,
project_uuid,
area_uuid,
tags,
limit,
)
.await
.map_err(|e| McpError::database_operation_failed("logbook_search", e))?;
let json = serde_json::to_string_pretty(&tasks)
.map_err(|e| McpError::serialization_failed("logbook_search serialization", e))?;
Ok(CallToolResult {
content: vec![Content::Text { text: json }],
is_error: false,
})
}
async fn handle_create_task(&self, args: Value) -> McpResult<CallToolResult> {
let request: things3_core::CreateTaskRequest =
serde_json::from_value(args).map_err(|e| {
McpError::invalid_parameter(
"request",
format!("Failed to parse create task request: {e}"),
)
})?;
let uuid = self
.db
.create_task(request)
.await
.map_err(|e| McpError::database_operation_failed("create_task", e))?;
let response = serde_json::json!({
"uuid": uuid,
"message": "Task created successfully"
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("create_task response", e))?,
}],
is_error: false,
})
}
async fn handle_update_task(&self, args: Value) -> McpResult<CallToolResult> {
let request: things3_core::UpdateTaskRequest =
serde_json::from_value(args).map_err(|e| {
McpError::invalid_parameter(
"request",
format!("Failed to parse update task request: {e}"),
)
})?;
self.db
.update_task(request)
.await
.map_err(|e| McpError::database_operation_failed("update_task", e))?;
let response = serde_json::json!({
"message": "Task updated successfully"
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("update_task response", e))?,
}],
is_error: false,
})
}
async fn handle_complete_task(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
self.db
.complete_task(&uuid)
.await
.map_err(|e| McpError::database_operation_failed("complete_task", e))?;
let response = serde_json::json!({
"message": "Task completed successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("complete_task response", e))?,
}],
is_error: false,
})
}
async fn handle_uncomplete_task(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
self.db
.uncomplete_task(&uuid)
.await
.map_err(|e| McpError::database_operation_failed("uncomplete_task", e))?;
let response = serde_json::json!({
"message": "Task marked as incomplete successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("uncomplete_task response", e))?,
}],
is_error: false,
})
}
async fn handle_delete_task(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
let child_handling_str = args
.get("child_handling")
.and_then(|v| v.as_str())
.unwrap_or("error");
let child_handling = match child_handling_str {
"cascade" => DeleteChildHandling::Cascade,
"orphan" => DeleteChildHandling::Orphan,
_ => DeleteChildHandling::Error,
};
self.db
.delete_task(&uuid, child_handling)
.await
.map_err(|e| McpError::database_operation_failed("delete_task", e))?;
let response = serde_json::json!({
"message": "Task deleted successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("delete_task response", e))?,
}],
is_error: false,
})
}
async fn handle_bulk_move(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid_strs: Vec<String> = args
.get("task_uuids")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
.iter()
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
})
})
.collect::<McpResult<Vec<_>>>()?;
let project_uuid = args
.get("project_uuid")
.and_then(|v| v.as_str())
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("project_uuid", format!("Invalid UUID: {e}"))
})
})
.transpose()?;
let area_uuid = args
.get("area_uuid")
.and_then(|v| v.as_str())
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
})
})
.transpose()?;
let request = things3_core::models::BulkMoveRequest {
task_uuids,
project_uuid,
area_uuid,
};
let result = self
.db
.bulk_move(request)
.await
.map_err(|e| McpError::database_operation_failed("bulk_move", e))?;
let response = serde_json::json!({
"success": result.success,
"processed_count": result.processed_count,
"message": result.message
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("bulk_move response", e))?,
}],
is_error: false,
})
}
async fn handle_bulk_update_dates(&self, args: Value) -> McpResult<CallToolResult> {
use chrono::NaiveDate;
let task_uuid_strs: Vec<String> = args
.get("task_uuids")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
.iter()
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
})
})
.collect::<McpResult<Vec<_>>>()?;
let start_date = args
.get("start_date")
.and_then(|v| v.as_str())
.map(|s| {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("start_date", format!("Invalid date format: {e}"))
})
})
.transpose()?;
let deadline = args
.get("deadline")
.and_then(|v| v.as_str())
.map(|s| {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("deadline", format!("Invalid date format: {e}"))
})
})
.transpose()?;
let clear_start_date = args
.get("clear_start_date")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let clear_deadline = args
.get("clear_deadline")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let request = things3_core::models::BulkUpdateDatesRequest {
task_uuids,
start_date,
deadline,
clear_start_date,
clear_deadline,
};
let result = self
.db
.bulk_update_dates(request)
.await
.map_err(|e| McpError::database_operation_failed("bulk_update_dates", e))?;
let response = serde_json::json!({
"success": result.success,
"processed_count": result.processed_count,
"message": result.message
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("bulk_update_dates response", e))?,
}],
is_error: false,
})
}
async fn handle_bulk_complete(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid_strs: Vec<String> = args
.get("task_uuids")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
.iter()
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
})
})
.collect::<McpResult<Vec<_>>>()?;
let request = things3_core::models::BulkCompleteRequest { task_uuids };
let result = self
.db
.bulk_complete(request)
.await
.map_err(|e| McpError::database_operation_failed("bulk_complete", e))?;
let response = serde_json::json!({
"success": result.success,
"processed_count": result.processed_count,
"message": result.message
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("bulk_complete response", e))?,
}],
is_error: false,
})
}
async fn handle_bulk_delete(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid_strs: Vec<String> = args
.get("task_uuids")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError::invalid_parameter("task_uuids", "Array of UUIDs is required"))?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let task_uuids: Vec<uuid::Uuid> = task_uuid_strs
.iter()
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("task_uuids", format!("Invalid UUID: {e}"))
})
})
.collect::<McpResult<Vec<_>>>()?;
let request = things3_core::models::BulkDeleteRequest { task_uuids };
let result = self
.db
.bulk_delete(request)
.await
.map_err(|e| McpError::database_operation_failed("bulk_delete", e))?;
let response = serde_json::json!({
"success": result.success,
"processed_count": result.processed_count,
"message": result.message
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("bulk_delete response", e))?,
}],
is_error: false,
})
}
async fn handle_create_project(&self, args: Value) -> McpResult<CallToolResult> {
let title = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("title", "Project title is required"))?
.to_string();
let notes = args.get("notes").and_then(|v| v.as_str()).map(String::from);
let area_uuid = args
.get("area_uuid")
.and_then(|v| v.as_str())
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
})
})
.transpose()?;
let start_date = args
.get("start_date")
.and_then(|v| v.as_str())
.map(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("start_date", format!("Invalid date: {e}"))
})
})
.transpose()?;
let deadline = args
.get("deadline")
.and_then(|v| v.as_str())
.map(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("deadline", format!("Invalid date: {e}"))
})
})
.transpose()?;
let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
});
let request = things3_core::models::CreateProjectRequest {
title,
notes,
area_uuid,
start_date,
deadline,
tags,
};
let uuid = self
.db
.create_project(request)
.await
.map_err(|e| McpError::database_operation_failed("create_project", e))?;
let response = serde_json::json!({
"message": "Project created successfully",
"uuid": uuid.to_string()
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("create_project response", e))?,
}],
is_error: false,
})
}
async fn handle_update_project(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
let title = args.get("title").and_then(|v| v.as_str()).map(String::from);
let notes = args.get("notes").and_then(|v| v.as_str()).map(String::from);
let area_uuid = args
.get("area_uuid")
.and_then(|v| v.as_str())
.map(|s| {
uuid::Uuid::parse_str(s).map_err(|e| {
McpError::invalid_parameter("area_uuid", format!("Invalid UUID: {e}"))
})
})
.transpose()?;
let start_date = args
.get("start_date")
.and_then(|v| v.as_str())
.map(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("start_date", format!("Invalid date: {e}"))
})
})
.transpose()?;
let deadline = args
.get("deadline")
.and_then(|v| v.as_str())
.map(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
McpError::invalid_parameter("deadline", format!("Invalid date: {e}"))
})
})
.transpose()?;
let tags = args.get("tags").and_then(|v| v.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
});
let request = things3_core::models::UpdateProjectRequest {
uuid,
title,
notes,
area_uuid,
start_date,
deadline,
tags,
};
self.db
.update_project(request)
.await
.map_err(|e| McpError::database_operation_failed("update_project", e))?;
let response = serde_json::json!({
"message": "Project updated successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("update_project response", e))?,
}],
is_error: false,
})
}
async fn handle_complete_project(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
let child_handling_str = args
.get("child_handling")
.and_then(|v| v.as_str())
.unwrap_or("error");
let child_handling = match child_handling_str {
"cascade" => things3_core::models::ProjectChildHandling::Cascade,
"orphan" => things3_core::models::ProjectChildHandling::Orphan,
_ => things3_core::models::ProjectChildHandling::Error,
};
self.db
.complete_project(&uuid, child_handling)
.await
.map_err(|e| McpError::database_operation_failed("complete_project", e))?;
let response = serde_json::json!({
"message": "Project completed successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("complete_project response", e))?,
}],
is_error: false,
})
}
async fn handle_delete_project(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
let child_handling_str = args
.get("child_handling")
.and_then(|v| v.as_str())
.unwrap_or("error");
let child_handling = match child_handling_str {
"cascade" => things3_core::models::ProjectChildHandling::Cascade,
"orphan" => things3_core::models::ProjectChildHandling::Orphan,
_ => things3_core::models::ProjectChildHandling::Error,
};
self.db
.delete_project(&uuid, child_handling)
.await
.map_err(|e| McpError::database_operation_failed("delete_project", e))?;
let response = serde_json::json!({
"message": "Project deleted successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("delete_project response", e))?,
}],
is_error: false,
})
}
async fn handle_create_area(&self, args: Value) -> McpResult<CallToolResult> {
let title = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("title", "Area title is required"))?
.to_string();
let request = things3_core::models::CreateAreaRequest { title };
let uuid = self
.db
.create_area(request)
.await
.map_err(|e| McpError::database_operation_failed("create_area", e))?;
let response = serde_json::json!({
"message": "Area created successfully",
"uuid": uuid.to_string()
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("create_area response", e))?,
}],
is_error: false,
})
}
async fn handle_update_area(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
let title = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("title", "Title is required"))?
.to_string();
let request = things3_core::models::UpdateAreaRequest { uuid, title };
self.db
.update_area(request)
.await
.map_err(|e| McpError::database_operation_failed("update_area", e))?;
let response = serde_json::json!({
"message": "Area updated successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("update_area response", e))?,
}],
is_error: false,
})
}
async fn handle_delete_area(&self, args: Value) -> McpResult<CallToolResult> {
let uuid_str = args
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("uuid", "UUID is required"))?;
let uuid = uuid::Uuid::parse_str(uuid_str)
.map_err(|e| McpError::invalid_parameter("uuid", format!("Invalid UUID: {e}")))?;
self.db
.delete_area(&uuid)
.await
.map_err(|e| McpError::database_operation_failed("delete_area", e))?;
let response = serde_json::json!({
"message": "Area deleted successfully",
"uuid": uuid_str
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("delete_area response", e))?,
}],
is_error: false,
})
}
async fn handle_get_productivity_metrics(&self, args: Value) -> McpResult<CallToolResult> {
let days = usize::try_from(
args.get("days")
.and_then(serde_json::Value::as_u64)
.unwrap_or(7),
)
.unwrap_or(7);
let db = &self.db;
let inbox_tasks = db
.get_inbox(None)
.await
.map_err(|e| McpError::database_operation_failed("get_inbox for metrics", e))?;
let today_tasks = db
.get_today(None)
.await
.map_err(|e| McpError::database_operation_failed("get_today for metrics", e))?;
let projects = db
.get_projects(None)
.await
.map_err(|e| McpError::database_operation_failed("get_projects for metrics", e))?;
let areas = db
.get_areas()
.await
.map_err(|e| McpError::database_operation_failed("get_areas for metrics", e))?;
let _ = db;
let metrics = serde_json::json!({
"period_days": days,
"inbox_tasks_count": inbox_tasks.len(),
"today_tasks_count": today_tasks.len(),
"projects_count": projects.len(),
"areas_count": areas.len(),
"completed_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Completed).count(),
"incomplete_tasks": projects.iter().filter(|p| p.status == things3_core::TaskStatus::Incomplete).count(),
"timestamp": chrono::Utc::now()
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&metrics).map_err(|e| {
McpError::serialization_failed("productivity_metrics serialization", e)
})?,
}],
is_error: false,
})
}
async fn handle_export_data(&self, args: Value) -> McpResult<CallToolResult> {
let format = args
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("format"))?;
let data_type = args
.get("data_type")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("data_type"))?;
let db = &self.db;
let export_data =
match data_type {
"tasks" => {
let inbox = db.get_inbox(None).await.map_err(|e| {
McpError::database_operation_failed("get_inbox for export", e)
})?;
let today = db.get_today(None).await.map_err(|e| {
McpError::database_operation_failed("get_today for export", e)
})?;
serde_json::json!({
"inbox": inbox,
"today": today
})
}
"projects" => {
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for export", e)
})?;
serde_json::json!({ "projects": projects })
}
"areas" => {
let areas = db.get_areas().await.map_err(|e| {
McpError::database_operation_failed("get_areas for export", e)
})?;
serde_json::json!({ "areas": areas })
}
"all" => {
let inbox = db.get_inbox(None).await.map_err(|e| {
McpError::database_operation_failed("get_inbox for export", e)
})?;
let today = db.get_today(None).await.map_err(|e| {
McpError::database_operation_failed("get_today for export", e)
})?;
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for export", e)
})?;
let areas = db.get_areas().await.map_err(|e| {
McpError::database_operation_failed("get_areas for export", e)
})?;
let _ = db;
serde_json::json!({
"inbox": inbox,
"today": today,
"projects": projects,
"areas": areas
})
}
_ => {
return Err(McpError::invalid_data_type(
data_type,
"tasks, projects, areas, all",
))
}
};
let result = match format {
"json" => serde_json::to_string_pretty(&export_data)
.map_err(|e| McpError::serialization_failed("export_data serialization", e))?,
"csv" => "CSV export not yet implemented".to_string(),
"markdown" => "Markdown export not yet implemented".to_string(),
_ => return Err(McpError::invalid_format(format, "json, csv, markdown")),
};
Ok(CallToolResult {
content: vec![Content::Text { text: result }],
is_error: false,
})
}
fn handle_bulk_create_tasks(args: &Value) -> McpResult<CallToolResult> {
let tasks = args
.get("tasks")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError::missing_parameter("tasks"))?;
let response = serde_json::json!({
"message": "Bulk task creation not yet implemented",
"tasks_count": tasks.len(),
"status": "placeholder"
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("bulk_create_tasks response", e))?,
}],
is_error: false,
})
}
async fn handle_get_recent_tasks(&self, args: Value) -> McpResult<CallToolResult> {
let limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX));
let hours = i64::try_from(
args.get("hours")
.and_then(serde_json::Value::as_u64)
.unwrap_or(24),
)
.unwrap_or(24);
let tasks = self
.db
.get_inbox(limit)
.await
.map_err(|e| McpError::database_operation_failed("get_recent_tasks", e))?;
let response = serde_json::json!({
"message": "Recent tasks (using inbox as proxy)",
"hours_lookback": hours,
"tasks": tasks
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("get_recent_tasks response", e))?,
}],
is_error: false,
})
}
async fn handle_backup_database(&self, args: Value) -> McpResult<CallToolResult> {
let backup_dir = args
.get("backup_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("backup_dir"))?;
let description = args.get("description").and_then(|v| v.as_str());
let backup_path = std::path::Path::new(backup_dir);
let metadata = self
.backup_manager
.lock()
.await
.create_backup(backup_path, description)
.map_err(|e| {
McpError::backup_operation_failed(
"create_backup",
things3_core::ThingsError::unknown(e.to_string()),
)
})?;
let response = serde_json::json!({
"message": "Backup created successfully",
"backup_path": metadata.backup_path,
"file_size": metadata.file_size,
"created_at": metadata.created_at
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("backup_database response", e))?,
}],
is_error: false,
})
}
async fn handle_restore_database(&self, args: Value) -> McpResult<CallToolResult> {
let backup_path = args
.get("backup_path")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("backup_path"))?;
let backup_file = std::path::Path::new(backup_path);
self.backup_manager
.lock()
.await
.restore_backup(backup_file)
.map_err(|e| {
McpError::backup_operation_failed(
"restore_backup",
things3_core::ThingsError::unknown(e.to_string()),
)
})?;
let response = serde_json::json!({
"message": "Database restored successfully",
"backup_path": backup_path
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("restore_database response", e))?,
}],
is_error: false,
})
}
async fn handle_list_backups(&self, args: Value) -> McpResult<CallToolResult> {
let backup_dir = args
.get("backup_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("backup_dir"))?;
let backup_path = std::path::Path::new(backup_dir);
let backups = self
.backup_manager
.lock()
.await
.list_backups(backup_path)
.map_err(|e| {
McpError::backup_operation_failed(
"list_backups",
things3_core::ThingsError::unknown(e.to_string()),
)
})?;
let response = serde_json::json!({
"backups": backups,
"count": backups.len()
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("list_backups response", e))?,
}],
is_error: false,
})
}
async fn handle_get_performance_stats(&self, _args: Value) -> McpResult<CallToolResult> {
let monitor = self.performance_monitor.lock().await;
let stats = monitor.get_all_stats();
let summary = monitor.get_summary();
drop(monitor);
let response = serde_json::json!({
"summary": summary,
"operation_stats": stats
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("performance_stats response", e))?,
}],
is_error: false,
})
}
async fn handle_get_system_metrics(&self, _args: Value) -> McpResult<CallToolResult> {
let metrics = self
.performance_monitor
.lock()
.await
.get_system_metrics()
.map_err(|e| {
McpError::performance_monitoring_failed(
"get_system_metrics",
things3_core::ThingsError::unknown(e.to_string()),
)
})?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&metrics)
.map_err(|e| McpError::serialization_failed("system_metrics response", e))?,
}],
is_error: false,
})
}
async fn handle_get_cache_stats(&self, _args: Value) -> McpResult<CallToolResult> {
let stats = self.cache.lock().await.get_stats();
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&stats)
.map_err(|e| McpError::serialization_failed("cache_stats response", e))?,
}],
is_error: false,
})
}
async fn handle_search_tags_tool(&self, args: Value) -> McpResult<CallToolResult> {
let query: String = args
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("query", "Missing 'query' parameter"))?
.to_string();
let include_similar = args
.get("include_similar")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let min_similarity = args
.get("min_similarity")
.and_then(|v| v.as_f64())
.unwrap_or(0.7) as f32;
let tags = if include_similar {
self.db
.find_similar_tags(&query, min_similarity)
.await
.map_err(|e| McpError::database_operation_failed("search_tags", e))?
.into_iter()
.map(|tm| tm.tag)
.collect()
} else {
self.db
.search_tags(&query)
.await
.map_err(|e| McpError::database_operation_failed("search_tags", e))?
};
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&tags)
.map_err(|e| McpError::serialization_failed("tags", e))?,
}],
is_error: false,
})
}
async fn handle_get_tag_suggestions(&self, args: Value) -> McpResult<CallToolResult> {
let title: String = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("title", "Missing 'title' parameter"))?
.to_string();
use things3_core::database::tag_utils::normalize_tag_title;
let normalized = normalize_tag_title(&title);
let exact_match = self
.db
.find_tag_by_normalized_title(&normalized)
.await
.map_err(|e| McpError::database_operation_failed("get_tag_suggestions", e))?;
let similar_tags = self
.db
.find_similar_tags(&normalized, 0.7)
.await
.map_err(|e| McpError::database_operation_failed("get_tag_suggestions", e))?;
let recommendation = if exact_match.is_some() {
"use_existing"
} else if !similar_tags.is_empty() {
"consider_similar"
} else {
"create_new"
};
let response = serde_json::json!({
"exact_match": exact_match,
"similar_tags": similar_tags,
"recommendation": recommendation
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("tag_suggestions", e))?,
}],
is_error: false,
})
}
async fn handle_get_popular_tags(&self, args: Value) -> McpResult<CallToolResult> {
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
let tags = self
.db
.get_popular_tags(limit)
.await
.map_err(|e| McpError::database_operation_failed("get_popular_tags", e))?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&tags)
.map_err(|e| McpError::serialization_failed("popular_tags", e))?,
}],
is_error: false,
})
}
async fn handle_get_recent_tags(&self, args: Value) -> McpResult<CallToolResult> {
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
let tags = self
.db
.get_recent_tags(limit)
.await
.map_err(|e| McpError::database_operation_failed("get_recent_tags", e))?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&tags)
.map_err(|e| McpError::serialization_failed("recent_tags", e))?,
}],
is_error: false,
})
}
async fn handle_create_tag(&self, args: Value) -> McpResult<CallToolResult> {
let title: String = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::invalid_parameter("title", "Missing 'title' parameter"))?
.to_string();
let shortcut: Option<String> = args
.get("shortcut")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let parent_uuid: Option<Uuid> = args
.get("parent_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok());
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let request = things3_core::models::CreateTagRequest {
title,
shortcut,
parent_uuid,
};
let result = if force {
let uuid = self
.db
.create_tag_force(request)
.await
.map_err(|e| McpError::database_operation_failed("create_tag", e))?;
serde_json::json!({
"status": "created",
"uuid": uuid,
"message": "Tag created successfully (duplicate check skipped)"
})
} else {
match self
.db
.create_tag_smart(request)
.await
.map_err(|e| McpError::database_operation_failed("create_tag", e))?
{
things3_core::models::TagCreationResult::Created { uuid, .. } => {
serde_json::json!({
"status": "created",
"uuid": uuid,
"message": "Tag created successfully"
})
}
things3_core::models::TagCreationResult::Existing { tag, .. } => {
serde_json::json!({
"status": "existing",
"uuid": tag.uuid,
"tag": tag,
"message": "Tag already exists"
})
}
things3_core::models::TagCreationResult::SimilarFound {
similar_tags,
requested_title,
} => {
serde_json::json!({
"status": "similar_found",
"similar_tags": similar_tags,
"requested_title": requested_title,
"message": "Similar tags found. Use force=true to create anyway."
})
}
}
};
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&result)
.map_err(|e| McpError::serialization_failed("create_tag_response", e))?,
}],
is_error: false,
})
}
async fn handle_update_tag(&self, args: Value) -> McpResult<CallToolResult> {
let uuid: Uuid = args
.get("uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
})?;
let title: Option<String> = args
.get("title")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let shortcut: Option<String> = args
.get("shortcut")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let parent_uuid: Option<Uuid> = args
.get("parent_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok());
let request = things3_core::models::UpdateTagRequest {
uuid,
title,
shortcut,
parent_uuid,
};
self.db
.update_tag(request)
.await
.map_err(|e| McpError::database_operation_failed("update_tag", e))?;
let response = serde_json::json!({
"message": "Tag updated successfully",
"uuid": uuid
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("update_tag_response", e))?,
}],
is_error: false,
})
}
async fn handle_delete_tag(&self, args: Value) -> McpResult<CallToolResult> {
let uuid: Uuid = args
.get("uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
})?;
let remove_from_tasks = args
.get("remove_from_tasks")
.and_then(|v| v.as_bool())
.unwrap_or(false);
self.db
.delete_tag(&uuid, remove_from_tasks)
.await
.map_err(|e| McpError::database_operation_failed("delete_tag", e))?;
let response = serde_json::json!({
"message": "Tag deleted successfully",
"uuid": uuid
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("delete_tag_response", e))?,
}],
is_error: false,
})
}
async fn handle_merge_tags(&self, args: Value) -> McpResult<CallToolResult> {
let source_uuid: Uuid = args
.get("source_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter(
"source_uuid",
"Missing or invalid 'source_uuid' parameter",
)
})?;
let target_uuid: Uuid = args
.get("target_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter(
"target_uuid",
"Missing or invalid 'target_uuid' parameter",
)
})?;
self.db
.merge_tags(&source_uuid, &target_uuid)
.await
.map_err(|e| McpError::database_operation_failed("merge_tags", e))?;
let response = serde_json::json!({
"message": "Tags merged successfully",
"source_uuid": source_uuid,
"target_uuid": target_uuid
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("merge_tags_response", e))?,
}],
is_error: false,
})
}
async fn handle_add_tag_to_task(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid: Uuid = args
.get("task_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
})?;
let tag_title: String = args
.get("tag_title")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_parameter("tag_title", "Missing 'tag_title' parameter")
})?
.to_string();
let result = self
.db
.add_tag_to_task(&task_uuid, &tag_title)
.await
.map_err(|e| McpError::database_operation_failed("add_tag_to_task", e))?;
let response = match result {
things3_core::models::TagAssignmentResult::Assigned { tag_uuid } => {
serde_json::json!({
"status": "assigned",
"tag_uuid": tag_uuid,
"message": "Tag added to task successfully"
})
}
things3_core::models::TagAssignmentResult::Suggestions { similar_tags } => {
serde_json::json!({
"status": "suggestions",
"similar_tags": similar_tags,
"message": "Similar tags found. Please confirm or use a different tag."
})
}
};
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("add_tag_to_task_response", e))?,
}],
is_error: false,
})
}
async fn handle_remove_tag_from_task(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid: Uuid = args
.get("task_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
})?;
let tag_title: String = args
.get("tag_title")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_parameter("tag_title", "Missing 'tag_title' parameter")
})?
.to_string();
self.db
.remove_tag_from_task(&task_uuid, &tag_title)
.await
.map_err(|e| McpError::database_operation_failed("remove_tag_from_task", e))?;
let response = serde_json::json!({
"message": "Tag removed from task successfully",
"task_uuid": task_uuid,
"tag_title": tag_title
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response).map_err(|e| {
McpError::serialization_failed("remove_tag_from_task_response", e)
})?,
}],
is_error: false,
})
}
async fn handle_set_task_tags(&self, args: Value) -> McpResult<CallToolResult> {
let task_uuid: Uuid = args
.get("task_uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("task_uuid", "Missing or invalid 'task_uuid' parameter")
})?;
let tag_titles: Vec<String> = args
.get("tag_titles")
.and_then(|v| v.as_array())
.ok_or_else(|| {
McpError::invalid_parameter("tag_titles", "Missing 'tag_titles' parameter")
})?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let suggestions = self
.db
.set_task_tags(&task_uuid, tag_titles.clone())
.await
.map_err(|e| McpError::database_operation_failed("set_task_tags", e))?;
let response = serde_json::json!({
"message": "Task tags updated successfully",
"task_uuid": task_uuid,
"tags": tag_titles,
"suggestions": suggestions
});
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&response)
.map_err(|e| McpError::serialization_failed("set_task_tags_response", e))?,
}],
is_error: false,
})
}
async fn handle_get_tag_statistics(&self, args: Value) -> McpResult<CallToolResult> {
let uuid: Uuid = args
.get("uuid")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
.ok_or_else(|| {
McpError::invalid_parameter("uuid", "Missing or invalid 'uuid' parameter")
})?;
let stats = self
.db
.get_tag_statistics(&uuid)
.await
.map_err(|e| McpError::database_operation_failed("get_tag_statistics", e))?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&stats)
.map_err(|e| McpError::serialization_failed("tag_statistics", e))?,
}],
is_error: false,
})
}
async fn handle_find_duplicate_tags(&self, args: Value) -> McpResult<CallToolResult> {
let min_similarity = args
.get("min_similarity")
.and_then(|v| v.as_f64())
.unwrap_or(0.85) as f32;
let duplicates = self
.db
.find_duplicate_tags(min_similarity)
.await
.map_err(|e| McpError::database_operation_failed("find_duplicate_tags", e))?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&duplicates)
.map_err(|e| McpError::serialization_failed("duplicate_tags", e))?,
}],
is_error: false,
})
}
async fn handle_get_tag_completions(&self, args: Value) -> McpResult<CallToolResult> {
let partial_input: String = args
.get("partial_input")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::invalid_parameter("partial_input", "Missing 'partial_input' parameter")
})?
.to_string();
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let completions = self
.db
.get_tag_completions(&partial_input, limit)
.await
.map_err(|e| McpError::database_operation_failed("get_tag_completions", e))?;
Ok(CallToolResult {
content: vec![Content::Text {
text: serde_json::to_string_pretty(&completions)
.map_err(|e| McpError::serialization_failed("tag_completions", e))?,
}],
is_error: false,
})
}
fn get_available_prompts() -> Vec<Prompt> {
vec![
Self::create_task_review_prompt(),
Self::create_project_planning_prompt(),
Self::create_productivity_analysis_prompt(),
Self::create_backup_strategy_prompt(),
]
}
fn create_task_review_prompt() -> Prompt {
Prompt {
name: "task_review".to_string(),
description: "Review task for completeness and clarity".to_string(),
arguments: serde_json::json!({
"type": "object",
"properties": {
"task_title": {
"type": "string",
"description": "The title of the task to review"
},
"task_notes": {
"type": "string",
"description": "Optional notes or description of the task"
},
"context": {
"type": "string",
"description": "Optional context about the task or project"
}
},
"required": ["task_title"]
}),
}
}
fn create_project_planning_prompt() -> Prompt {
Prompt {
name: "project_planning".to_string(),
description: "Help plan projects with tasks and deadlines".to_string(),
arguments: serde_json::json!({
"type": "object",
"properties": {
"project_title": {
"type": "string",
"description": "The title of the project to plan"
},
"project_description": {
"type": "string",
"description": "Description of what the project aims to achieve"
},
"deadline": {
"type": "string",
"description": "Optional deadline for the project"
},
"complexity": {
"type": "string",
"description": "Project complexity level",
"enum": ["simple", "medium", "complex"]
}
},
"required": ["project_title"]
}),
}
}
fn create_productivity_analysis_prompt() -> Prompt {
Prompt {
name: "productivity_analysis".to_string(),
description: "Analyze productivity patterns".to_string(),
arguments: serde_json::json!({
"type": "object",
"properties": {
"time_period": {
"type": "string",
"description": "Time period to analyze",
"enum": ["week", "month", "quarter", "year"]
},
"focus_area": {
"type": "string",
"description": "Specific area to focus analysis on",
"enum": ["completion_rate", "time_management", "task_distribution", "all"]
},
"include_recommendations": {
"type": "boolean",
"description": "Whether to include improvement recommendations"
}
},
"required": ["time_period"]
}),
}
}
fn create_backup_strategy_prompt() -> Prompt {
Prompt {
name: "backup_strategy".to_string(),
description: "Suggest backup strategies".to_string(),
arguments: serde_json::json!({
"type": "object",
"properties": {
"data_volume": {
"type": "string",
"description": "Estimated data volume",
"enum": ["small", "medium", "large"]
},
"frequency": {
"type": "string",
"description": "Desired backup frequency",
"enum": ["daily", "weekly", "monthly"]
},
"retention_period": {
"type": "string",
"description": "How long to keep backups",
"enum": ["1_month", "3_months", "6_months", "1_year", "indefinite"]
},
"storage_preference": {
"type": "string",
"description": "Preferred storage type",
"enum": ["local", "cloud", "hybrid"]
}
},
"required": ["data_volume", "frequency"]
}),
}
}
async fn handle_prompt_request(&self, request: GetPromptRequest) -> McpResult<GetPromptResult> {
let prompt_name = &request.name;
let arguments = request.arguments.unwrap_or_default();
match prompt_name.as_str() {
"task_review" => self.handle_task_review_prompt(arguments).await,
"project_planning" => self.handle_project_planning_prompt(arguments).await,
"productivity_analysis" => self.handle_productivity_analysis_prompt(arguments).await,
"backup_strategy" => self.handle_backup_strategy_prompt(arguments).await,
_ => Err(McpError::prompt_not_found(prompt_name)),
}
}
async fn handle_task_review_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
let task_title = args
.get("task_title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("task_title"))?;
let task_notes = args.get("task_notes").and_then(|v| v.as_str());
let context = args.get("context").and_then(|v| v.as_str());
let db = &self.db;
let inbox_tasks = db
.get_inbox(Some(5))
.await
.map_err(|e| McpError::database_operation_failed("get_inbox for task_review", e))?;
let today_tasks = db
.get_today(Some(5))
.await
.map_err(|e| McpError::database_operation_failed("get_today for task_review", e))?;
let _ = db;
let prompt_text = format!(
"# Task Review: {}\n\n\
## Current Task Details\n\
- **Title**: {}\n\
- **Notes**: {}\n\
- **Context**: {}\n\n\
## Review Checklist\n\
Please review this task for:\n\
1. **Clarity**: Is the task title clear and actionable?\n\
2. **Completeness**: Does it have all necessary details?\n\
3. **Priority**: How urgent/important is this task?\n\
4. **Dependencies**: Are there any prerequisites?\n\
5. **Time Estimate**: How long should this take?\n\n\
## Current Context\n\
- **Inbox Tasks**: {} tasks\n\
- **Today's Tasks**: {} tasks\n\n\
## Recommendations\n\
Based on the current workload and task details, provide specific recommendations for:\n\
- Improving task clarity\n\
- Breaking down complex tasks\n\
- Setting appropriate deadlines\n\
- Managing dependencies\n\n\
## Next Steps\n\
Suggest concrete next steps to move this task forward effectively.",
task_title,
task_title,
task_notes.unwrap_or("No notes provided"),
context.unwrap_or("No additional context"),
inbox_tasks.len(),
today_tasks.len()
);
Ok(GetPromptResult {
content: vec![Content::Text { text: prompt_text }],
is_error: false,
})
}
async fn handle_project_planning_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
let project_title = args
.get("project_title")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("project_title"))?;
let project_description = args.get("project_description").and_then(|v| v.as_str());
let deadline = args.get("deadline").and_then(|v| v.as_str());
let complexity = args
.get("complexity")
.and_then(|v| v.as_str())
.unwrap_or("medium");
let db = &self.db;
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for project_planning", e)
})?;
let areas = db.get_areas().await.map_err(|e| {
McpError::database_operation_failed("get_areas for project_planning", e)
})?;
let _ = db;
let prompt_text = format!(
"# Project Planning: {}\n\n\
## Project Overview\n\
- **Title**: {}\n\
- **Description**: {}\n\
- **Deadline**: {}\n\
- **Complexity**: {}\n\n\
## Planning Framework\n\
Please help plan this project by:\n\
1. **Breaking down** the project into manageable tasks\n\
2. **Estimating** time requirements for each task\n\
3. **Identifying** dependencies between tasks\n\
4. **Suggesting** milestones and checkpoints\n\
5. **Recommending** project organization (areas, tags, etc.)\n\n\
## Current Context\n\
- **Existing Projects**: {} projects\n\
- **Available Areas**: {} areas\n\n\
## Task Breakdown\n\
Create a detailed task list with:\n\
- Clear, actionable task titles\n\
- Estimated time for each task\n\
- Priority levels\n\
- Dependencies\n\
- Suggested deadlines\n\n\
## Project Organization\n\
Suggest:\n\
- Appropriate area for this project\n\
- Useful tags for organization\n\
- Project structure and hierarchy\n\n\
## Risk Assessment\n\
Identify potential challenges and mitigation strategies.\n\n\
## Success Metrics\n\
Define how to measure project success and completion.",
project_title,
project_title,
project_description.unwrap_or("No description provided"),
deadline.unwrap_or("No deadline specified"),
complexity,
projects.len(),
areas.len()
);
Ok(GetPromptResult {
content: vec![Content::Text { text: prompt_text }],
is_error: false,
})
}
async fn handle_productivity_analysis_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
let time_period = args
.get("time_period")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("time_period"))?;
let focus_area = args
.get("focus_area")
.and_then(|v| v.as_str())
.unwrap_or("all");
let include_recommendations = args
.get("include_recommendations")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
let db = &self.db;
let inbox_tasks = db.get_inbox(None).await.map_err(|e| {
McpError::database_operation_failed("get_inbox for productivity_analysis", e)
})?;
let today_tasks = db.get_today(None).await.map_err(|e| {
McpError::database_operation_failed("get_today for productivity_analysis", e)
})?;
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for productivity_analysis", e)
})?;
let areas = db.get_areas().await.map_err(|e| {
McpError::database_operation_failed("get_areas for productivity_analysis", e)
})?;
let _ = db;
let completed_tasks = projects
.iter()
.filter(|p| p.status == things3_core::TaskStatus::Completed)
.count();
let incomplete_tasks = projects
.iter()
.filter(|p| p.status == things3_core::TaskStatus::Incomplete)
.count();
let prompt_text = format!(
"# Productivity Analysis - {}\n\n\
## Analysis Period: {}\n\
## Focus Area: {}\n\n\
## Current Data Overview\n\
- **Inbox Tasks**: {} tasks\n\
- **Today's Tasks**: {} tasks\n\
- **Total Projects**: {} projects\n\
- **Areas**: {} areas\n\
- **Completed Tasks**: {} tasks\n\
- **Incomplete Tasks**: {} tasks\n\n\
## Analysis Framework\n\
Please analyze productivity patterns focusing on:\n\n\
### 1. Task Completion Patterns\n\
- Completion rates over the period\n\
- Task types that are completed vs. delayed\n\
- Time patterns in task completion\n\n\
### 2. Workload Distribution\n\
- Balance between different areas/projects\n\
- Task complexity distribution\n\
- Deadline adherence patterns\n\n\
### 3. Time Management\n\
- Task scheduling effectiveness\n\
- Inbox vs. scheduled task completion\n\
- Overdue task patterns\n\n\
### 4. Project Progress\n\
- Project completion rates\n\
- Project complexity vs. completion time\n\
- Area-based productivity differences\n\n\
## Key Insights\n\
Identify:\n\
- Peak productivity times\n\
- Most/least productive areas\n\
- Common bottlenecks\n\
- Success patterns\n\n\
## Recommendations\n\
{}",
time_period,
time_period,
focus_area,
inbox_tasks.len(),
today_tasks.len(),
projects.len(),
areas.len(),
completed_tasks,
incomplete_tasks,
if include_recommendations {
"Provide specific, actionable recommendations for:\n\
- Improving task completion rates\n\
- Better time management\n\
- Workload balancing\n\
- Process optimization\n\
- Goal setting and tracking"
} else {
"Focus on analysis without recommendations"
}
);
Ok(GetPromptResult {
content: vec![Content::Text { text: prompt_text }],
is_error: false,
})
}
async fn handle_backup_strategy_prompt(&self, args: Value) -> McpResult<GetPromptResult> {
let data_volume = args
.get("data_volume")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("data_volume"))?;
let frequency = args
.get("frequency")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError::missing_parameter("frequency"))?;
let retention_period = args
.get("retention_period")
.and_then(|v| v.as_str())
.unwrap_or("3_months");
let storage_preference = args
.get("storage_preference")
.and_then(|v| v.as_str())
.unwrap_or("hybrid");
let db = &self.db;
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for backup_strategy", e)
})?;
let areas = db
.get_areas()
.await
.map_err(|e| McpError::database_operation_failed("get_areas for backup_strategy", e))?;
let _ = db;
let prompt_text = format!(
"# Backup Strategy Recommendation\n\n\
## Requirements\n\
- **Data Volume**: {}\n\
- **Backup Frequency**: {}\n\
- **Retention Period**: {}\n\
- **Storage Preference**: {}\n\n\
## Current Data Context\n\
- **Projects**: {} projects\n\
- **Areas**: {} areas\n\
- **Database Type**: SQLite (Things 3)\n\n\
## Backup Strategy Analysis\n\n\
### 1. Data Assessment\n\
Analyze the current data volume and growth patterns:\n\
- Database size estimation\n\
- Growth rate projections\n\
- Critical data identification\n\n\
### 2. Backup Frequency Optimization\n\
For {} frequency backups:\n\
- Optimal timing considerations\n\
- Incremental vs. full backup strategy\n\
- Performance impact analysis\n\n\
### 3. Storage Strategy\n\
For {} storage preference:\n\
- Local storage recommendations\n\
- Cloud storage options\n\
- Hybrid approach benefits\n\
- Cost considerations\n\n\
### 4. Retention Policy\n\
For {} retention period:\n\
- Data lifecycle management\n\
- Compliance considerations\n\
- Storage optimization\n\n\
## Recommended Implementation\n\
Provide specific recommendations for:\n\
- Backup tools and software\n\
- Storage locations and providers\n\
- Automation setup\n\
- Monitoring and alerting\n\
- Recovery procedures\n\n\
## Risk Mitigation\n\
Address:\n\
- Data loss prevention\n\
- Backup verification\n\
- Disaster recovery planning\n\
- Security considerations\n\n\
## Cost Analysis\n\
Estimate costs for:\n\
- Storage requirements\n\
- Backup software/tools\n\
- Cloud services\n\
- Maintenance overhead",
data_volume,
frequency,
retention_period,
storage_preference,
projects.len(),
areas.len(),
frequency,
storage_preference,
retention_period
);
Ok(GetPromptResult {
content: vec![Content::Text { text: prompt_text }],
is_error: false,
})
}
fn get_available_resources() -> Vec<Resource> {
vec![
Resource {
uri: "things://inbox".to_string(),
name: "Inbox Tasks".to_string(),
description: "Current inbox tasks from Things 3".to_string(),
mime_type: Some("application/json".to_string()),
},
Resource {
uri: "things://projects".to_string(),
name: "All Projects".to_string(),
description: "All projects in Things 3".to_string(),
mime_type: Some("application/json".to_string()),
},
Resource {
uri: "things://areas".to_string(),
name: "All Areas".to_string(),
description: "All areas in Things 3".to_string(),
mime_type: Some("application/json".to_string()),
},
Resource {
uri: "things://today".to_string(),
name: "Today's Tasks".to_string(),
description: "Tasks scheduled for today".to_string(),
mime_type: Some("application/json".to_string()),
},
]
}
async fn handle_resource_read(
&self,
request: ReadResourceRequest,
) -> McpResult<ReadResourceResult> {
let uri = &request.uri;
let db = &self.db;
let data = match uri.as_str() {
"things://inbox" => {
let tasks = db.get_inbox(None).await.map_err(|e| {
McpError::database_operation_failed("get_inbox for resource", e)
})?;
serde_json::to_string_pretty(&tasks).map_err(|e| {
McpError::serialization_failed("inbox resource serialization", e)
})?
}
"things://projects" => {
let projects = db.get_projects(None).await.map_err(|e| {
McpError::database_operation_failed("get_projects for resource", e)
})?;
serde_json::to_string_pretty(&projects).map_err(|e| {
McpError::serialization_failed("projects resource serialization", e)
})?
}
"things://areas" => {
let areas = db.get_areas().await.map_err(|e| {
McpError::database_operation_failed("get_areas for resource", e)
})?;
serde_json::to_string_pretty(&areas).map_err(|e| {
McpError::serialization_failed("areas resource serialization", e)
})?
}
"things://today" => {
let tasks = db.get_today(None).await.map_err(|e| {
McpError::database_operation_failed("get_today for resource", e)
})?;
let _ = db;
serde_json::to_string_pretty(&tasks).map_err(|e| {
McpError::serialization_failed("today resource serialization", e)
})?
}
_ => {
return Err(McpError::resource_not_found(uri));
}
};
Ok(ReadResourceResult {
contents: vec![Content::Text { text: data }],
})
}
pub async fn handle_jsonrpc_request(
&self,
request: serde_json::Value,
) -> things3_core::Result<Option<serde_json::Value>> {
use serde_json::json;
let method = request["method"].as_str().ok_or_else(|| {
things3_core::ThingsError::unknown("Missing method in JSON-RPC request".to_string())
})?;
let params = request["params"].clone();
let is_notification = request.get("id").is_none();
if is_notification {
match method {
"notifications/initialized" => {
return Ok(None);
}
_ => {
return Ok(None);
}
}
}
let id = request["id"].clone();
let result = match method {
"initialize" => {
json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false },
"prompts": { "listChanged": false }
},
"serverInfo": {
"name": "things3-mcp",
"version": env!("CARGO_PKG_VERSION")
}
})
}
"tools/list" => {
let tools_result = self.list_tools().map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to list tools: {}", e))
})?;
json!(tools_result.tools)
}
"tools/call" => {
let tool_name = params["name"]
.as_str()
.ok_or_else(|| {
things3_core::ThingsError::unknown(
"Missing tool name in params".to_string(),
)
})?
.to_string();
let arguments = params["arguments"].clone();
let call_request = CallToolRequest {
name: tool_name,
arguments: Some(arguments),
};
let call_result = self.call_tool(call_request).await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to call tool: {}", e))
})?;
json!(call_result)
}
"resources/list" => {
let resources_result = self.list_resources().map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to list resources: {}", e))
})?;
json!(resources_result.resources)
}
"resources/read" => {
let uri = params["uri"]
.as_str()
.ok_or_else(|| {
things3_core::ThingsError::unknown("Missing URI in params".to_string())
})?
.to_string();
let read_request = ReadResourceRequest { uri };
let read_result = self.read_resource(read_request).await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to read resource: {}", e))
})?;
json!(read_result)
}
"prompts/list" => {
let prompts_result = self.list_prompts().map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to list prompts: {}", e))
})?;
json!(prompts_result.prompts)
}
"prompts/get" => {
let prompt_name = params["name"]
.as_str()
.ok_or_else(|| {
things3_core::ThingsError::unknown(
"Missing prompt name in params".to_string(),
)
})?
.to_string();
let arguments = params.get("arguments").cloned();
let get_request = GetPromptRequest {
name: prompt_name,
arguments,
};
let get_result = self.get_prompt(get_request).await.map_err(|e| {
things3_core::ThingsError::unknown(format!("Failed to get prompt: {}", e))
})?;
json!(get_result)
}
_ => {
return Ok(Some(json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": format!("Method not found: {}", method)
}
})));
}
};
Ok(Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": result
})))
}
}