add todo now adds alarms to start and end of todo

more sensible name for submodule `caldav` of `caldav`
This commit is contained in:
2026-05-03 20:28:31 +02:00
parent 3dd897c422
commit 3d8975cfc1
4 changed files with 59 additions and 30 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ icalendar = { version = "0.17.10", optional = true, features = ["chrono-tz"] }
hyper-util = { version = "0.1.20", optional = true } hyper-util = { version = "0.1.20", optional = true }
http = { version = "1.4.0", optional = true } http = { version = "1.4.0", optional = true }
hyper-rustls = { version = "0.27.7", optional = true } hyper-rustls = { version = "0.27.7", optional = true }
toml = "1.1.2+spec-1.1.0" toml = "1.1.2"
[features] [features]
default = ["datetime", "caldav"] default = ["datetime", "caldav"]
@@ -54,7 +54,7 @@ pub(crate) enum CalendarError {
impl From<CalendarError> for ErrorData { impl From<CalendarError> for ErrorData {
fn from(value: CalendarError) -> Self { fn from(value: CalendarError) -> Self {
use crate::caldav::caldav::CalendarError::*; use crate::caldav::client::CalendarError::*;
match value { match value {
UserPrincipalError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), UserPrincipalError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None),
+32 -17
View File
@@ -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::{ use crate::server_handler::{
McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string, McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string,
}; };
use http::Uri; use http::Uri;
use icalendar::{Todo}; use icalendar::Todo;
use libdav::dav::FoundCollection; use libdav::dav::FoundCollection;
use rmcp::handler::server::tool::ToolRouter; use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters; use rmcp::handler::server::wrapper::Parameters;
@@ -11,13 +14,12 @@ use rmcp::model::{CallToolResult, ErrorCode, Implementation, ServerCapabilities,
use rmcp::schemars::JsonSchema; use rmcp::schemars::JsonSchema;
use rmcp::{ErrorData, schemars}; use rmcp::{ErrorData, schemars};
use rmcp::{ServerHandler, serde_json, tool, tool_handler, tool_router}; use rmcp::{ServerHandler, serde_json, tool, tool_handler, tool_router};
use serde::{Deserialize}; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
use toml::Value; use toml::Value;
use toml::map::Map; use toml::map::Map;
use crate::caldav::todo::McpTodo;
mod caldav; mod client;
mod todo; mod todo;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -25,7 +27,7 @@ pub(crate) struct CalDavHandler {
client: AuthorizedCaldavClient, client: AuthorizedCaldavClient,
calendar_references: Vec<CalendarReference>, calendar_references: Vec<CalendarReference>,
tool_router: ToolRouter<Self>, tool_router: ToolRouter<Self>,
default_calendar_name: String default_calendar_name: String,
} }
#[derive(Debug, Clone)] #[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 { Ok(Self {
client, client,
calendar_references, calendar_references,
tool_router: Self::tool_router(), tool_router: Self::tool_router(),
default_calendar_name: default_calendar default_calendar_name: default_calendar,
}) })
} }
} }
impl CalDavHandler { impl CalDavHandler {
fn get_calendar_by_name(&self, calendar_name: Option<String>) -> Result<FoundCollection, ErrorData> { fn get_calendar_by_name(
&self,
calendar_name: Option<String>,
) -> Result<FoundCollection, ErrorData> {
let calendar_name = calendar_name.unwrap_or(self.default_calendar_name.clone()); let calendar_name = calendar_name.unwrap_or(self.default_calendar_name.clone());
self.calendar_references self.calendar_references
@@ -119,12 +125,15 @@ struct GetTodosParameters {
struct AddTodoParameters { struct AddTodoParameters {
#[schemars(description = "uses default if null")] #[schemars(description = "uses default if null")]
calendar_name: Option<String>, calendar_name: Option<String>,
todo: McpTodo todo: McpTodo,
} }
#[tool_router] #[tool_router]
impl CalDavHandler { 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<CallToolResult, ErrorData> { async fn get_calendars(&self) -> Result<CallToolResult, ErrorData> {
let names = self let names = self
.calendar_references .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( async fn get_todos(
&self, &self,
parameters: Parameters<GetTodosParameters>, parameters: Parameters<GetTodosParameters>,
@@ -148,7 +160,9 @@ impl CalDavHandler {
let mut todos: Vec<McpTodo> = Vec::new(); let mut todos: Vec<McpTodo> = Vec::new();
for component in components { 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()?); todos.push(todo_component.clone().try_into()?);
} }
} }
@@ -157,7 +171,10 @@ impl CalDavHandler {
} }
#[tool(description = "add a new todo to a calendar")] #[tool(description = "add a new todo to a calendar")]
async fn add_todo(&self, parameters: Parameters<AddTodoParameters>) -> Result<CallToolResult, ErrorData> { async fn add_todo(
&self,
parameters: Parameters<AddTodoParameters>,
) -> Result<CallToolResult, ErrorData> {
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?; let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
let todo: Todo = parameters.0.todo.into(); let todo: Todo = parameters.0.todo.into();
@@ -171,9 +188,7 @@ impl CalDavHandler {
impl ServerHandler for CalDavHandler { impl ServerHandler for CalDavHandler {
fn get_info(&self) -> ServerInfo { fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions( .with_instructions("enables interaction with calendars".to_string())
"enables interaction with calendars".to_string(),
)
.with_server_info(Implementation::new("caldav", env!("CARGO_PKG_VERSION"))) .with_server_info(Implementation::new("caldav", env!("CARGO_PKG_VERSION")))
} }
} }
+24 -10
View File
@@ -1,17 +1,17 @@
use rmcp::schemars;
use chrono::{DateTime, NaiveTime, TimeZone}; 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::ErrorData;
use rmcp::model::ErrorCode; use rmcp::model::ErrorCode;
use rmcp::schemars;
use rmcp::schemars::JsonSchema; use rmcp::schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)] #[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
pub (in crate::caldav) struct McpTodo { pub(in crate::caldav) struct McpTodo {
pub (in crate::caldav) summary: Option<String>, pub(in crate::caldav) summary: Option<String>,
pub (in crate::caldav) description: Option<String>, pub(in crate::caldav) description: Option<String>,
pub (in crate::caldav) start: Option<DateTime<chrono::Local>>, pub(in crate::caldav) start: Option<DateTime<chrono::Local>>,
pub (in crate::caldav) due: Option<DateTime<chrono::Local>>, pub(in crate::caldav) due: Option<DateTime<chrono::Local>>,
} }
impl McpTodo { impl McpTodo {
@@ -32,12 +32,18 @@ impl McpTodo {
.try_into_utc() .try_into_utc()
.map(DateTime::<chrono::Local>::from), .map(DateTime::<chrono::Local>::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 { match local_datetime {
Some(local_datetime) => Ok(Some(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<Todo> for McpTodo {
type Error = ErrorData; type Error = ErrorData;
fn try_from(value: Todo) -> Result<Self, Self::Error> { fn try_from(value: Todo) -> Result<Self, Self::Error> {
Ok(McpTodo{ Ok(McpTodo {
summary: value.get_summary().map(|s| s.to_string()), summary: value.get_summary().map(|s| s.to_string()),
description: value.get_description().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())?, start: Self::date_perhaps_time_to_local_dt(value.get_start())?,
@@ -75,6 +81,14 @@ impl From<McpTodo> for Todo {
todo.due(due.to_utc()); 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 todo
} }
} }