# Conflicts:
#	mcp_server_collection/Cargo.toml
This commit is contained in:
2026-05-24 13:02:13 +02:00
9 changed files with 398 additions and 53 deletions
@@ -0,0 +1,36 @@
use chrono::{DateTime, NaiveTime, TimeZone};
use icalendar::DatePerhapsTime;
use rmcp::ErrorData;
use rmcp::model::ErrorCode;
pub(in crate::caldav) fn date_perhaps_time_to_local_dt(
date_perhaps_time: Option<DatePerhapsTime>,
) -> Result<Option<DateTime<chrono::Local>>, ErrorData> {
use DatePerhapsTime;
if date_perhaps_time.is_none() {
return Ok(None);
}
use icalendar::CalendarDateTime::*;
let local_datetime = match date_perhaps_time.unwrap() {
DatePerhapsTime::DateTime(datetime) => match datetime {
Floating(naive_dt) => chrono::Local.from_local_datetime(&naive_dt).single(), // assuming local timezone
timezone_dt => timezone_dt
.try_into_utc()
.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
};
match local_datetime {
Some(local_datetime) => Ok(Some(local_datetime)),
None => Err(ErrorData::new(
ErrorCode::INVALID_PARAMS,
"DateTime conversion failed",
None,
)),
}
}
+63
View File
@@ -0,0 +1,63 @@
use crate::caldav::datetime_conversion::date_perhaps_time_to_local_dt;
use chrono::DateTime;
use icalendar::Trigger;
use icalendar::{Alarm, Component, Event, EventLike, Related};
use rmcp::schemars::JsonSchema;
use rmcp::{ErrorData, schemars};
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
pub(in crate::caldav) struct McpEvent {
pub(in crate::caldav) summary: String,
pub(in crate::caldav) description: Option<String>,
pub(in crate::caldav) location: Option<String>,
pub(in crate::caldav) start: Option<DateTime<chrono::Local>>,
pub(in crate::caldav) end: Option<DateTime<chrono::Local>>,
}
impl TryFrom<Event> for McpEvent {
type Error = ErrorData;
fn try_from(value: Event) -> Result<Self, Self::Error> {
Ok(McpEvent {
summary: value.get_summary().map(String::from).unwrap_or_default(),
description: value.get_description().map(String::from),
location: value.get_location().map(String::from),
start: date_perhaps_time_to_local_dt(value.get_start())?,
end: date_perhaps_time_to_local_dt(value.get_end())?,
})
}
}
impl From<McpEvent> for Event {
fn from(value: McpEvent) -> Self {
let mut event = Event::new();
event.summary(value.summary.as_str());
if let Some(description) = value.description {
event.description(description.as_str());
}
if let Some(start) = value.start {
event.starts(start.to_utc());
}
if let Some(end) = value.end {
event.ends(end.to_utc());
}
if let Some(location) = value.location {
event.location(location.as_str());
}
event.alarm(Alarm::display(
event
.get_summary()
.unwrap_or(event.get_summary().unwrap_or_default()),
Trigger::Duration(chrono::Duration::zero(), Some(Related::Start)),
));
event
}
}
+74 -3
View File
@@ -1,3 +1,5 @@
use tracing::Level;
use chrono::{Duration, Local};
use crate::caldav::client::{
AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components,
};
@@ -6,7 +8,7 @@ use crate::server_handler::{
McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string,
};
use http::Uri;
use icalendar::Todo;
use icalendar::{Event, EventLike, Todo};
use libdav::dav::FoundCollection;
use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
@@ -18,8 +20,11 @@ use serde::Deserialize;
use std::str::FromStr;
use toml::Value;
use toml::map::Map;
use crate::caldav::event::McpEvent;
mod client;
mod datetime_conversion;
mod event;
mod todo;
#[derive(Debug, Clone)]
@@ -116,7 +121,7 @@ impl CalDavHandler {
}
#[derive(JsonSchema, Deserialize, Debug)]
struct GetTodosParameters {
struct GetCalendarComponentParameters {
#[schemars(description = "uses default if null")]
calendar_name: Option<String>,
}
@@ -128,6 +133,13 @@ struct AddTodoParameters {
todo: McpTodo,
}
#[derive(JsonSchema, Deserialize, Debug)]
struct AddEventParameters {
#[schemars(description = "uses default if null")]
calendar_name: Option<String>,
event: McpEvent,
}
#[tool_router]
impl CalDavHandler {
#[tool(
@@ -152,7 +164,7 @@ impl CalDavHandler {
)]
async fn get_todos(
&self,
parameters: Parameters<GetTodosParameters>,
parameters: Parameters<GetCalendarComponentParameters>,
) -> Result<CallToolResult, ErrorData> {
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
@@ -182,6 +194,65 @@ impl CalDavHandler {
Ok(CallToolResult::success(vec![]))
}
#[tool(description = "gets events of next 7 days")]
async fn get_upcoming_events(&self, parameters: Parameters<GetCalendarComponentParameters>) -> Result<CallToolResult, ErrorData> {
const DAYS_DELTA: Duration = Duration::days(7);
const RECURRENCE_LIMIT: u16 = 10;
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
let components = get_components(&self.client, &calendar).await?;
let mut events: Vec<McpEvent> = Vec::new();
for component in components {
if let Some(event_component) = component.as_event() {
let event: McpEvent = event_component.clone().try_into()?;
let lower_bound = Local::now().with_timezone(&icalendar::Tz::Local(chrono::Local));
let upper_bound = lower_bound + DAYS_DELTA;
let recurrence = event_component.get_recurrence();
match recurrence {
Ok(recurrence) => {
let recurrence_result = recurrence.after(lower_bound).before(upper_bound).all(RECURRENCE_LIMIT);
for date in recurrence_result.dates {
let mut event_instance = event.clone();
event_instance.start = Some(date.with_timezone(&chrono::Local{}));
if let Some(start) = event.start && let Some(end) = event.end {
let event_duration = end-start;
event_instance.end = Some(event_instance.start.unwrap() + event_duration);
}
events.push(event_instance);
}
},
Err(error) => {
tracing::event!(Level::INFO, "recurrence error occured. Event '{event:?}' is ignored: {error}");
}
}
}
}
events.sort_by_key(|event|event.start);
Ok(CallToolResult::structured(serde_json::json!(events)))
}
#[tool(description = "add a new event to a calendar")]
async fn add_event(
&self,
parameters: Parameters<AddEventParameters>,
) -> Result<CallToolResult, ErrorData> {
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
let event: Event = parameters.0.event.into();
upload_components(&self.client, &calendar, vec![event]).await?;
Ok(CallToolResult::success(vec![]))
}
}
#[tool_handler]
+11 -47
View File
@@ -1,62 +1,28 @@
use chrono::{DateTime, NaiveTime, TimeZone};
use icalendar::{Alarm, Component, DatePerhapsTime, EventLike, Related, Todo, Trigger};
use crate::caldav::datetime_conversion::date_perhaps_time_to_local_dt;
use chrono::DateTime;
use icalendar::{Alarm, Component, 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<String>,
pub(in crate::caldav) summary: String,
pub(in crate::caldav) description: Option<String>,
pub(in crate::caldav) start: Option<DateTime<chrono::Local>>,
pub(in crate::caldav) due: Option<DateTime<chrono::Local>>,
}
impl McpTodo {
fn date_perhaps_time_to_local_dt(
date_perhaps_time: Option<DatePerhapsTime>,
) -> Result<Option<DateTime<chrono::Local>>, ErrorData> {
use DatePerhapsTime;
if date_perhaps_time.is_none() {
return Ok(None);
}
use icalendar::CalendarDateTime::*;
let local_datetime = match date_perhaps_time.unwrap() {
DatePerhapsTime::DateTime(datetime) => match datetime {
Floating(naive_dt) => chrono::Local.from_local_datetime(&naive_dt).single(), // assuming local timezone
timezone_dt => timezone_dt
.try_into_utc()
.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
};
match local_datetime {
Some(local_datetime) => Ok(Some(local_datetime)),
None => Err(ErrorData::new(
ErrorCode::INVALID_PARAMS,
"DateTime conversion failed",
None,
)),
}
}
}
impl TryFrom<Todo> for McpTodo {
type Error = ErrorData;
fn try_from(value: Todo) -> Result<Self, Self::Error> {
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())?,
due: Self::date_perhaps_time_to_local_dt(value.get_due())?,
summary: value.get_summary().map(String::from).unwrap_or_default(),
description: value.get_description().map(String::from),
start: date_perhaps_time_to_local_dt(value.get_start())?,
due: date_perhaps_time_to_local_dt(value.get_due())?,
})
}
}
@@ -65,9 +31,7 @@ impl From<McpTodo> for Todo {
fn from(value: McpTodo) -> Self {
let mut todo = Todo::new();
if let Some(summary) = value.summary {
todo.summary(summary.as_str());
}
todo.summary(value.summary.as_str());
if let Some(description) = value.description {
todo.description(description.as_str());
@@ -84,8 +48,8 @@ impl From<McpTodo> for Todo {
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)),
.unwrap_or(todo.get_description().unwrap_or_default()),
Trigger::Duration(chrono::Duration::zero(), Some(related)),
));
}