From 3d8975cfc115b4f50ee75a976df1966aff0b0a73 Mon Sep 17 00:00:00 2001 From: milan Date: Sun, 3 May 2026 20:28:31 +0200 Subject: [PATCH] add todo now adds alarms to start and end of todo more sensible name for submodule `caldav` of `caldav` --- mcp_server_collection/Cargo.toml | 2 +- .../src/caldav/{caldav.rs => client.rs} | 2 +- mcp_server_collection/src/caldav/mod.rs | 49 ++++++++++++------- mcp_server_collection/src/caldav/todo.rs | 36 +++++++++----- 4 files changed, 59 insertions(+), 30 deletions(-) rename mcp_server_collection/src/caldav/{caldav.rs => client.rs} (98%) diff --git a/mcp_server_collection/Cargo.toml b/mcp_server_collection/Cargo.toml index 545eec5..00dbeb1 100644 --- a/mcp_server_collection/Cargo.toml +++ b/mcp_server_collection/Cargo.toml @@ -21,7 +21,7 @@ icalendar = { version = "0.17.10", optional = true, features = ["chrono-tz"] } hyper-util = { version = "0.1.20", optional = true } http = { version = "1.4.0", optional = true } hyper-rustls = { version = "0.27.7", optional = true } -toml = "1.1.2+spec-1.1.0" +toml = "1.1.2" [features] default = ["datetime", "caldav"] diff --git a/mcp_server_collection/src/caldav/caldav.rs b/mcp_server_collection/src/caldav/client.rs similarity index 98% rename from mcp_server_collection/src/caldav/caldav.rs rename to mcp_server_collection/src/caldav/client.rs index f69b9fb..e963b5b 100644 --- a/mcp_server_collection/src/caldav/caldav.rs +++ b/mcp_server_collection/src/caldav/client.rs @@ -54,7 +54,7 @@ pub(crate) enum CalendarError { impl From for ErrorData { fn from(value: CalendarError) -> Self { - use crate::caldav::caldav::CalendarError::*; + use crate::caldav::client::CalendarError::*; match value { UserPrincipalError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), diff --git a/mcp_server_collection/src/caldav/mod.rs b/mcp_server_collection/src/caldav/mod.rs index 59dc1bb..8f21a1b 100644 --- a/mcp_server_collection/src/caldav/mod.rs +++ b/mcp_server_collection/src/caldav/mod.rs @@ -1,9 +1,12 @@ -use crate::caldav::caldav::{AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components}; +use crate::caldav::client::{ + AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components, +}; +use crate::caldav::todo::McpTodo; use crate::server_handler::{ McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string, }; use http::Uri; -use icalendar::{Todo}; +use icalendar::Todo; use libdav::dav::FoundCollection; use rmcp::handler::server::tool::ToolRouter; use rmcp::handler::server::wrapper::Parameters; @@ -11,13 +14,12 @@ use rmcp::model::{CallToolResult, ErrorCode, Implementation, ServerCapabilities, use rmcp::schemars::JsonSchema; use rmcp::{ErrorData, schemars}; use rmcp::{ServerHandler, serde_json, tool, tool_handler, tool_router}; -use serde::{Deserialize}; +use serde::Deserialize; use std::str::FromStr; use toml::Value; use toml::map::Map; -use crate::caldav::todo::McpTodo; -mod caldav; +mod client; mod todo; #[derive(Debug, Clone)] @@ -25,7 +27,7 @@ pub(crate) struct CalDavHandler { client: AuthorizedCaldavClient, calendar_references: Vec, tool_router: ToolRouter, - default_calendar_name: String + default_calendar_name: String, } #[derive(Debug, Clone)] @@ -82,19 +84,23 @@ impl McpServerHandler for CalDavHandler { }); } - let default_calendar = get_property_as_string("default-calendar", &additional_properties)?.to_string(); + let default_calendar = + get_property_as_string("default-calendar", &additional_properties)?.to_string(); Ok(Self { client, calendar_references, tool_router: Self::tool_router(), - default_calendar_name: default_calendar + default_calendar_name: default_calendar, }) } } impl CalDavHandler { - fn get_calendar_by_name(&self, calendar_name: Option) -> Result { + fn get_calendar_by_name( + &self, + calendar_name: Option, + ) -> Result { let calendar_name = calendar_name.unwrap_or(self.default_calendar_name.clone()); self.calendar_references @@ -119,12 +125,15 @@ struct GetTodosParameters { struct AddTodoParameters { #[schemars(description = "uses default if null")] calendar_name: Option, - todo: McpTodo + todo: McpTodo, } #[tool_router] impl CalDavHandler { - #[tool(description = "lists available calendars", annotations(read_only_hint = true))] + #[tool( + description = "lists available calendars", + annotations(read_only_hint = true) + )] async fn get_calendars(&self) -> Result { let names = self .calendar_references @@ -137,7 +146,10 @@ impl CalDavHandler { ))) } - #[tool(description = "get uncompleted todos", annotations(read_only_hint = true))] + #[tool( + description = "get uncompleted todos", + annotations(read_only_hint = true) + )] async fn get_todos( &self, parameters: Parameters, @@ -148,7 +160,9 @@ impl CalDavHandler { let mut todos: Vec = Vec::new(); for component in components { - if let Some(todo_component) = component.as_todo() && todo_component.get_completed().is_none() { + if let Some(todo_component) = component.as_todo() + && todo_component.get_completed().is_none() + { todos.push(todo_component.clone().try_into()?); } } @@ -157,7 +171,10 @@ impl CalDavHandler { } #[tool(description = "add a new todo to a calendar")] - async fn add_todo(&self, parameters: Parameters) -> Result { + async fn add_todo( + &self, + parameters: Parameters, + ) -> Result { let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?; let todo: Todo = parameters.0.todo.into(); @@ -171,9 +188,7 @@ impl CalDavHandler { impl ServerHandler for CalDavHandler { fn get_info(&self) -> ServerInfo { ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) - .with_instructions( - "enables interaction with calendars".to_string(), - ) + .with_instructions("enables interaction with calendars".to_string()) .with_server_info(Implementation::new("caldav", env!("CARGO_PKG_VERSION"))) } } diff --git a/mcp_server_collection/src/caldav/todo.rs b/mcp_server_collection/src/caldav/todo.rs index b962d0e..bc80ae7 100644 --- a/mcp_server_collection/src/caldav/todo.rs +++ b/mcp_server_collection/src/caldav/todo.rs @@ -1,17 +1,17 @@ -use rmcp::schemars; use chrono::{DateTime, NaiveTime, TimeZone}; -use icalendar::{Component, DatePerhapsTime, EventLike, Todo}; +use icalendar::{Alarm, Component, DatePerhapsTime, EventLike, Related, Todo, Trigger}; use rmcp::ErrorData; use rmcp::model::ErrorCode; +use rmcp::schemars; use rmcp::schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)] -pub (in crate::caldav) struct McpTodo { - pub (in crate::caldav) summary: Option, - pub (in crate::caldav) description: Option, - pub (in crate::caldav) start: Option>, - pub (in crate::caldav) due: Option>, +pub(in crate::caldav) struct McpTodo { + pub(in crate::caldav) summary: Option, + pub(in crate::caldav) description: Option, + pub(in crate::caldav) start: Option>, + pub(in crate::caldav) due: Option>, } impl McpTodo { @@ -32,12 +32,18 @@ impl McpTodo { .try_into_utc() .map(DateTime::::from), }, - DatePerhapsTime::Date(date) => chrono::Local.from_local_datetime(&date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())).single(), // assuming local timezone + DatePerhapsTime::Date(date) => chrono::Local + .from_local_datetime(&date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())) + .single(), // assuming local timezone }; match local_datetime { Some(local_datetime) => Ok(Some(local_datetime)), - None => Err(ErrorData::new(ErrorCode::INVALID_PARAMS, "DateTime conversion failed", None)), + None => Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "DateTime conversion failed", + None, + )), } } } @@ -46,7 +52,7 @@ impl TryFrom for McpTodo { type Error = ErrorData; fn try_from(value: Todo) -> Result { - Ok(McpTodo{ + Ok(McpTodo { summary: value.get_summary().map(|s| s.to_string()), description: value.get_description().map(|s| s.to_string()), start: Self::date_perhaps_time_to_local_dt(value.get_start())?, @@ -75,6 +81,14 @@ impl From for Todo { todo.due(due.to_utc()); } + for related in [Related::Start, Related::End] { + todo.alarm(Alarm::display( + todo.get_summary() + .unwrap_or(todo.get_description().unwrap_or("")), + Trigger::Duration(chrono::Duration::new(0, 0).unwrap(), Some(related)), + )); + } + todo } -} \ No newline at end of file +}