add event feature to caldav mcp server
This commit is contained in:
Generated
+15
@@ -1193,6 +1193,8 @@ dependencies = [
|
|||||||
"iso8601",
|
"iso8601",
|
||||||
"nom",
|
"nom",
|
||||||
"nom-language",
|
"nom-language",
|
||||||
|
"rrule",
|
||||||
|
"thiserror 2.0.18",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2600,6 +2602,19 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rrule"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtrb"
|
name = "rtrb"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|||||||
Generated
+170
-1
@@ -2,6 +2,21 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -24,6 +39,12 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -31,15 +52,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "own_assist_common"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "own_assist_common"
|
||||||
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -58,6 +116,23 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -96,6 +171,21 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -127,6 +217,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -166,12 +265,82 @@ version = "1.1.1+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
Generated
+27
@@ -739,6 +739,8 @@ dependencies = [
|
|||||||
"iso8601",
|
"iso8601",
|
||||||
"nom",
|
"nom",
|
||||||
"nom-language",
|
"nom-language",
|
||||||
|
"rrule",
|
||||||
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1328,6 +1330,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -1412,6 +1426,19 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rrule"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.40"
|
version = "0.23.40"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ uuid = { version = "1.23.0", features = ["v4"], optional = true }
|
|||||||
tower-http = { version = "0.6.8", features = ["auth"], optional = true }
|
tower-http = { version = "0.6.8", features = ["auth"], optional = true }
|
||||||
thiserror = { version = "2.0.18", optional = true }
|
thiserror = { version = "2.0.18", optional = true }
|
||||||
libdav = { version = "0.10.3", optional = true }
|
libdav = { version = "0.10.3", optional = true }
|
||||||
icalendar = { version = "0.17.10", optional = true, features = ["chrono-tz"] }
|
icalendar = { version = "0.17.10", optional = true, features = ["chrono-tz", "recurrence"] }
|
||||||
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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use tracing::Level;
|
||||||
|
use chrono::{Duration, Local};
|
||||||
use crate::caldav::client::{
|
use crate::caldav::client::{
|
||||||
AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components,
|
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,
|
McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string,
|
||||||
};
|
};
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use icalendar::Todo;
|
use icalendar::{Event, EventLike, 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;
|
||||||
@@ -18,8 +20,11 @@ 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::event::McpEvent;
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
|
mod datetime_conversion;
|
||||||
|
mod event;
|
||||||
mod todo;
|
mod todo;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -116,7 +121,7 @@ impl CalDavHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(JsonSchema, Deserialize, Debug)]
|
#[derive(JsonSchema, Deserialize, Debug)]
|
||||||
struct GetTodosParameters {
|
struct GetCalendarComponentParameters {
|
||||||
#[schemars(description = "uses default if null")]
|
#[schemars(description = "uses default if null")]
|
||||||
calendar_name: Option<String>,
|
calendar_name: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -128,6 +133,13 @@ struct AddTodoParameters {
|
|||||||
todo: McpTodo,
|
todo: McpTodo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema, Deserialize, Debug)]
|
||||||
|
struct AddEventParameters {
|
||||||
|
#[schemars(description = "uses default if null")]
|
||||||
|
calendar_name: Option<String>,
|
||||||
|
event: McpEvent,
|
||||||
|
}
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl CalDavHandler {
|
impl CalDavHandler {
|
||||||
#[tool(
|
#[tool(
|
||||||
@@ -152,7 +164,7 @@ impl CalDavHandler {
|
|||||||
)]
|
)]
|
||||||
async fn get_todos(
|
async fn get_todos(
|
||||||
&self,
|
&self,
|
||||||
parameters: Parameters<GetTodosParameters>,
|
parameters: Parameters<GetCalendarComponentParameters>,
|
||||||
) -> Result<CallToolResult, ErrorData> {
|
) -> 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)?;
|
||||||
|
|
||||||
@@ -182,6 +194,65 @@ impl CalDavHandler {
|
|||||||
|
|
||||||
Ok(CallToolResult::success(vec![]))
|
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]
|
#[tool_handler]
|
||||||
|
|||||||
@@ -1,62 +1,28 @@
|
|||||||
use chrono::{DateTime, NaiveTime, TimeZone};
|
use crate::caldav::datetime_conversion::date_perhaps_time_to_local_dt;
|
||||||
use icalendar::{Alarm, Component, DatePerhapsTime, EventLike, Related, Todo, Trigger};
|
use chrono::DateTime;
|
||||||
|
use icalendar::{Alarm, Component, EventLike, Related, Todo, Trigger};
|
||||||
use rmcp::ErrorData;
|
use rmcp::ErrorData;
|
||||||
use rmcp::model::ErrorCode;
|
|
||||||
use rmcp::schemars;
|
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: 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 {
|
|
||||||
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 {
|
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(String::from).unwrap_or_default(),
|
||||||
description: value.get_description().map(|s| s.to_string()),
|
description: value.get_description().map(String::from),
|
||||||
start: Self::date_perhaps_time_to_local_dt(value.get_start())?,
|
start: date_perhaps_time_to_local_dt(value.get_start())?,
|
||||||
due: Self::date_perhaps_time_to_local_dt(value.get_due())?,
|
due: date_perhaps_time_to_local_dt(value.get_due())?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,9 +31,7 @@ impl From<McpTodo> for Todo {
|
|||||||
fn from(value: McpTodo) -> Self {
|
fn from(value: McpTodo) -> Self {
|
||||||
let mut todo = Todo::new();
|
let mut todo = Todo::new();
|
||||||
|
|
||||||
if let Some(summary) = value.summary {
|
todo.summary(value.summary.as_str());
|
||||||
todo.summary(summary.as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(description) = value.description {
|
if let Some(description) = value.description {
|
||||||
todo.description(description.as_str());
|
todo.description(description.as_str());
|
||||||
@@ -84,8 +48,8 @@ impl From<McpTodo> for Todo {
|
|||||||
for related in [Related::Start, Related::End] {
|
for related in [Related::Start, Related::End] {
|
||||||
todo.alarm(Alarm::display(
|
todo.alarm(Alarm::display(
|
||||||
todo.get_summary()
|
todo.get_summary()
|
||||||
.unwrap_or(todo.get_description().unwrap_or("")),
|
.unwrap_or(todo.get_description().unwrap_or_default()),
|
||||||
Trigger::Duration(chrono::Duration::new(0, 0).unwrap(), Some(related)),
|
Trigger::Duration(chrono::Duration::zero(), Some(related)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user