From 3dd897c4221d961eecff973bd0c4e44d51f26805 Mon Sep 17 00:00:00 2001 From: milan Date: Sun, 3 May 2026 18:28:20 +0200 Subject: [PATCH] add caldav mcp server refactoring --- README.md | 3 +- common/src/config_loader.rs | 10 +- common/src/lib.rs | 2 +- mcp_server_collection/Cargo.lock | 744 +++++++++++++++++++- mcp_server_collection/Cargo.toml | 22 +- mcp_server_collection/src/caldav/caldav.rs | 149 ++++ mcp_server_collection/src/caldav/mod.rs | 179 +++++ mcp_server_collection/src/caldav/todo.rs | 80 +++ mcp_server_collection/src/config.rs | 11 +- mcp_server_collection/src/datetime.rs | 52 +- mcp_server_collection/src/main.rs | 99 ++- mcp_server_collection/src/server_handler.rs | 55 ++ own_mcp/src/mcp/chat.rs | 27 +- src/main.rs | 6 - 14 files changed, 1342 insertions(+), 97 deletions(-) create mode 100644 mcp_server_collection/src/caldav/caldav.rs create mode 100644 mcp_server_collection/src/caldav/mod.rs create mode 100644 mcp_server_collection/src/caldav/todo.rs create mode 100644 mcp_server_collection/src/server_handler.rs diff --git a/README.md b/README.md index 91f47ee..c146ae2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ## Assistant config ```toml +interface = "[Cli|Audio]" # optional, default is Cli permissions = [ {tool-name = "mcp-server:tool", permission="[Allowed|Ask|Denied]"} ] # optional @@ -14,7 +15,7 @@ system_prompt = "..." # optional context_size = 2048 # optional temperature = 0.8 # optional -[mcp-servers] +[[mcp-servers]] name="..." # optional, otherwise provided server name or random server name is chosen url="..." authorization="..." #optional diff --git a/common/src/config_loader.rs b/common/src/config_loader.rs index 0cea054..555e7bf 100644 --- a/common/src/config_loader.rs +++ b/common/src/config_loader.rs @@ -1,16 +1,18 @@ +use serde::de::DeserializeOwned; use std::path::Path; use thiserror::Error; -use serde::de::DeserializeOwned; #[derive(Debug, Error)] -pub enum ConfigLoadingError{ +pub enum ConfigLoadingError { #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] ParseError(#[from] toml::de::Error), } -pub fn config_from_file(filepath: impl AsRef) -> Result { +pub fn config_from_file( + filepath: impl AsRef, +) -> Result { let toml_string: String = std::fs::read_to_string(filepath)?; Ok(toml::from_str(&toml_string)?) -} \ No newline at end of file +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 5b3718b..2a50f86 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,2 +1,2 @@ pub mod config_loader; -pub use config_loader::config_from_file; \ No newline at end of file +pub use config_loader::config_from_file; diff --git a/mcp_server_collection/Cargo.lock b/mcp_server_collection/Cargo.lock index 4b18138..91c9eb8 100644 --- a/mcp_server_collection/Cargo.lock +++ b/mcp_server_collection/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -26,6 +32,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -49,6 +66,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -132,6 +171,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -149,7 +190,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures", - "rand_core", + "rand_core 0.10.0", ] [[package]] @@ -166,6 +207,44 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -181,6 +260,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "darling" version = "0.23.0" @@ -215,6 +318,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -226,6 +338,44 @@ dependencies = [ "syn", ] +[[package]] +name = "domain" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7ff15f82df7d5086fb15dfc1c1e96598a6ded9829840a9bcfa1fa3ccd8d01d" +dependencies = [ + "bumpalo", + "bytes", + "domain-macros", + "futures-util", + "hashbrown 0.14.5", + "log", + "moka", + "octseq", + "rand 0.8.6", + "smallvec", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "domain-macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1a6796ad411f6812d691955066ad27450196bfb181bb91b66a643cc3e8f5b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -245,7 +395,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -269,6 +440,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -357,6 +534,29 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -365,12 +565,21 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -455,6 +664,24 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] @@ -464,12 +691,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -496,6 +728,20 @@ dependencies = [ "cc", ] +[[package]] +name = "icalendar" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264fe856964b7a84076dec2d8ea29fce7c9b422968c9a2f404d4aa3771ae2179" +dependencies = [ + "chrono", + "chrono-tz", + "iso8601", + "nom", + "nom-language", + "uuid", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -623,12 +869,31 @@ dependencies = [ "serde_core", ] +[[package]] +name = "iso8601" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" +dependencies = [ + "nom", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.94" @@ -657,6 +922,23 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "libdav" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9992f07580da9541219ae9b94ac67de3b832953a8ca63855b4be5f44c5076ba8" +dependencies = [ + "domain", + "http", + "http-body-util", + "hyper", + "log", + "roxmltree", + "thiserror", + "tokio", + "tower-service", +] + [[package]] name = "litemap" version = "0.8.2" @@ -695,17 +977,26 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcp_server_collection" -version = "0.1.0" +version = "0.2.0" dependencies = [ "axum", "chrono", + "http", + "hyper-rustls", + "hyper-util", + "icalendar", + "libdav", "own_assist_common", "rmcp", "serde", + "thiserror", "tokio", "tokio-util", + "toml", + "tower-http", "tracing-subscriber", "url", + "uuid", ] [[package]] @@ -728,7 +1019,45 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "async-lock", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "event-listener", + "futures-util", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom", ] [[package]] @@ -737,9 +1066,15 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -749,12 +1084,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "octseq" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "own_assist_common" version = "0.1.0" @@ -764,6 +1115,12 @@ dependencies = [ "toml", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -799,12 +1156,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -814,6 +1195,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -842,12 +1238,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.0" @@ -855,8 +1268,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom", - "rand_core", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -911,6 +1343,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rmcp" version = "1.3.0" @@ -927,7 +1373,7 @@ dependencies = [ "http-body-util", "pastey", "pin-project-lite", - "rand", + "rand 0.10.0", "rmcp-macros", "schemars", "serde", @@ -955,6 +1401,63 @@ dependencies = [ "syn", ] +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -967,6 +1470,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "1.2.1" @@ -999,6 +1511,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1116,6 +1651,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -1135,7 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1163,6 +1704,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -1191,6 +1738,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "thiserror" version = "2.0.18" @@ -1220,6 +1773,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.3" @@ -1244,7 +1816,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1258,6 +1830,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1337,6 +1919,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "base64", + "bitflags", + "bytes", + "http", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1411,6 +2009,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1423,6 +2027,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1448,7 +2058,7 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -1459,6 +2069,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1621,6 +2240,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1630,6 +2258,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.1" @@ -1753,6 +2445,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.7" @@ -1774,6 +2486,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/mcp_server_collection/Cargo.toml b/mcp_server_collection/Cargo.toml index 007bab6..545eec5 100644 --- a/mcp_server_collection/Cargo.toml +++ b/mcp_server_collection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mcp_server_collection" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] @@ -9,11 +9,21 @@ chrono = "0.4.44" axum = "0.8.8" serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.51.0", features = ["full"] } -own_assist_common = {path="../common"} +own_assist_common = { path = "../common" } tokio-util = "0.7.18" -tracing-subscriber = {version = "0.3.23", features = ["env-filter"]} -url = {version = "2.5.8", features = ["serde"]} +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +url = { version = "2.5.8", features = ["serde"] } +uuid = { version = "1.23.0", features = ["v4"], optional = true } +tower-http = { version = "0.6.8", features = ["auth"], optional = true } +thiserror = { version = "2.0.18", optional = true } +libdav = { version = "0.10.3", optional = true } +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" [features] -default = ["datetime"] -datetime = [] \ No newline at end of file +default = ["datetime", "caldav"] +datetime = [] +caldav = ["dep:uuid", "dep:tower-http", "dep:thiserror", "dep:libdav", "dep:icalendar", "dep:hyper-util", "dep:http", "dep:hyper-rustls"] \ No newline at end of file diff --git a/mcp_server_collection/src/caldav/caldav.rs b/mcp_server_collection/src/caldav/caldav.rs new file mode 100644 index 0000000..f69b9fb --- /dev/null +++ b/mcp_server_collection/src/caldav/caldav.rs @@ -0,0 +1,149 @@ +use http::{StatusCode, Uri}; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use hyper_util::client::legacy::Client; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::rt::TokioExecutor; +use icalendar::{Calendar, CalendarComponent}; +use libdav::caldav::{FindCalendarHomeSet, FindCalendars, GetCalendarResources}; +use libdav::dav::{FoundCollection, PutResource, PutResourceResponse, WebDavClient, WebDavError}; +use libdav::{CalDavClient, FetchedResource}; +use rmcp::ErrorData; +use rmcp::model::ErrorCode; +use thiserror::Error; +use tower_http::auth::AddAuthorization; +use uuid::Uuid; + +pub(crate) type AuthorizedCaldavClient = + CalDavClient, String>>>; + +pub(crate) fn get_caldav_client( + webdav_uri: Uri, + username: &str, + password: &str, +) -> AuthorizedCaldavClient { + let https_connector: HttpsConnector = HttpsConnectorBuilder::new() + .with_native_roots() + .unwrap() + .https_only() + .enable_http1() + .build(); + + let https_client: Client, _> = + Client::builder(TokioExecutor::new()).build(https_connector); + + let https_client_with_auth = AddAuthorization::basic(https_client, username, password); + + CalDavClient::new(WebDavClient::new(webdav_uri, https_client_with_auth)) +} + +#[derive(Error, Debug)] +pub(crate) enum CalendarError { + #[error("failed to get user principal")] + UserPrincipalError( + #[from] libdav::dav::FindCurrentUserPrincipalError, + ), + #[error("User Principal does not exist")] + NoUserPrincipal, + #[error("webdav error")] + WebDavError(#[from] WebDavError), + #[error("unavailable resource")] + UnavailableResourceError(StatusCode), + #[error("failed Parsing a caldav resource")] + ParsingError(String), +} + +impl From for ErrorData { + fn from(value: CalendarError) -> Self { + use crate::caldav::caldav::CalendarError::*; + + match value { + UserPrincipalError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), + NoUserPrincipal => ErrorData::new( + ErrorCode::RESOURCE_NOT_FOUND, + "no user principal".to_string(), + None, + ), + WebDavError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), + UnavailableResourceError(e) => { + ErrorData::new(ErrorCode::RESOURCE_NOT_FOUND, e.to_string(), None) + } + ParsingError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), + } + } +} + +pub(crate) async fn find_calendars( + caldav_client: &AuthorizedCaldavClient, +) -> Result, CalendarError> { + use CalendarError::*; + + let user_principal = caldav_client + .find_current_user_principal() + .await? + .ok_or(NoUserPrincipal)?; + + let calendar_home_sets = caldav_client + .request(FindCalendarHomeSet::new(&user_principal)) + .await? + .home_sets; + + Ok(caldav_client + .request(FindCalendars::new(&calendar_home_sets[0])) + .await? + .calendars) +} + +pub(crate) async fn get_calendar_resources( + caldav_client: &AuthorizedCaldavClient, + calendar: &FoundCollection, +) -> Result, CalendarError> { + let calendar_resources = caldav_client + .request(GetCalendarResources::new(calendar.href.as_str())) + .await + .map_err(CalendarError::WebDavError)? + .resources; + + Ok(calendar_resources) +} + +pub(crate) async fn upload_components( + authorized_client: &AuthorizedCaldavClient, + calendar: &FoundCollection, + components: Vec>, +) -> Result { + println!("uploading components to {}", calendar.href); + let mut upload_calendar = Calendar::new(); + + for todo in components { + upload_calendar.push(todo); + } + + Ok(authorized_client + .request( + PutResource::new(format!("{}{}.ics", calendar.href, Uuid::new_v4()).as_str()) + .create(upload_calendar.to_string().leak(), "text/caldav"), + ) + .await?) +} + +pub(crate) async fn get_components( + authorized_client: &AuthorizedCaldavClient, + calendar: &FoundCollection, +) -> Result, CalendarError> { + use CalendarError::*; + + let mut components: Vec = Vec::new(); + + let resources = get_calendar_resources(authorized_client, calendar).await?; + for i in resources { + let calendar: Calendar = i + .content + .map_err(UnavailableResourceError)? + .data + .parse() + .map_err(ParsingError)?; + components.extend(calendar.components); + } + + Ok(components) +} diff --git a/mcp_server_collection/src/caldav/mod.rs b/mcp_server_collection/src/caldav/mod.rs new file mode 100644 index 0000000..59dc1bb --- /dev/null +++ b/mcp_server_collection/src/caldav/mod.rs @@ -0,0 +1,179 @@ +use crate::caldav::caldav::{AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components}; +use crate::server_handler::{ + McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string, +}; +use http::Uri; +use icalendar::{Todo}; +use libdav::dav::FoundCollection; +use rmcp::handler::server::tool::ToolRouter; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::{CallToolResult, ErrorCode, Implementation, ServerCapabilities, ServerInfo}; +use rmcp::schemars::JsonSchema; +use rmcp::{ErrorData, schemars}; +use rmcp::{ServerHandler, serde_json, tool, tool_handler, tool_router}; +use serde::{Deserialize}; +use std::str::FromStr; +use toml::Value; +use toml::map::Map; +use crate::caldav::todo::McpTodo; + +mod caldav; +mod todo; + +#[derive(Debug, Clone)] +pub(crate) struct CalDavHandler { + client: AuthorizedCaldavClient, + calendar_references: Vec, + tool_router: ToolRouter, + default_calendar_name: String +} + +#[derive(Debug, Clone)] +struct CalendarReference { + friendly_name: String, + collection: FoundCollection, +} + +impl McpServerHandler for CalDavHandler { + async fn new(additional_properties: Map) -> Result { + use McpServerHandlerError::*; + + let caldav_uri = get_property_as_string("caldav-uri", &additional_properties)?; + let caldav_uri = Uri::from_str(caldav_uri).map_err(|_| PropertyParseError { + value: caldav_uri.to_string(), + })?; + + let username = get_property_as_string("username", &additional_properties)?; + let password = get_property_as_string("password", &additional_properties)?; + + let client = get_caldav_client(caldav_uri, username, password); + + let mut calendar_references = Vec::new(); + + let configured_calendars = get_additional_property("calendars", &additional_properties)?; + let configured_calendars = configured_calendars.as_table().ok_or(WrongPropertyType { + value: configured_calendars.to_string(), + })?; + + let found_calendars = find_calendars(&client) + .await + .map_err(|e| InternalError(e.to_string()))?; + + for configured_cal in configured_calendars { + let friendly_name = configured_cal.0.to_owned(); + let href = configured_cal + .1 + .as_str() + .ok_or(WrongPropertyType { + value: friendly_name.to_string(), + })? + .to_string(); + + let found_cal = found_calendars + .iter() + .find(|calendar| calendar.href == href) + .ok_or(InternalError(format!( + "Could not find calendar href `{href}`. Available calendars: {found_calendars:?}" + )))?; + + calendar_references.push(CalendarReference { + friendly_name, + collection: found_cal.clone(), + }); + } + + 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 + }) + } +} + +impl CalDavHandler { + 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 + .iter() + .find(|calendar_ref| calendar_ref.friendly_name == calendar_name) + .map(|calendar_ref| calendar_ref.collection.clone()) + .ok_or(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "could not find calendar", + None, + )) + } +} + +#[derive(JsonSchema, Deserialize, Debug)] +struct GetTodosParameters { + #[schemars(description = "uses default if null")] + calendar_name: Option, +} + +#[derive(JsonSchema, Deserialize, Debug)] +struct AddTodoParameters { + #[schemars(description = "uses default if null")] + calendar_name: Option, + todo: McpTodo +} + +#[tool_router] +impl CalDavHandler { + #[tool(description = "lists available calendars", annotations(read_only_hint = true))] + async fn get_calendars(&self) -> Result { + let names = self + .calendar_references + .iter() + .map(|calendar_reference| calendar_reference.friendly_name.as_str()) + .collect::>(); + + Ok(CallToolResult::structured(serde_json::value::Value::from( + names, + ))) + } + + #[tool(description = "get uncompleted todos", annotations(read_only_hint = true))] + async fn get_todos( + &self, + parameters: Parameters, + ) -> Result { + let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?; + + let components = get_components(&self.client, &calendar).await?; + let mut todos: Vec = Vec::new(); + + for component in components { + if let Some(todo_component) = component.as_todo() && todo_component.get_completed().is_none() { + todos.push(todo_component.clone().try_into()?); + } + } + + Ok(CallToolResult::structured(serde_json::json!(todos))) + } + + #[tool(description = "add a new todo to a calendar")] + 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(); + upload_components(&self.client, &calendar, vec![todo]).await?; + + Ok(CallToolResult::success(vec![])) + } +} + +#[tool_handler] +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_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 new file mode 100644 index 0000000..b962d0e --- /dev/null +++ b/mcp_server_collection/src/caldav/todo.rs @@ -0,0 +1,80 @@ +use rmcp::schemars; +use chrono::{DateTime, NaiveTime, TimeZone}; +use icalendar::{Component, DatePerhapsTime, EventLike, Todo}; +use rmcp::ErrorData; +use rmcp::model::ErrorCode; +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>, +} + +impl McpTodo { + fn date_perhaps_time_to_local_dt( + date_perhaps_time: Option, + ) -> Result>, 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::::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 for McpTodo { + type Error = ErrorData; + + fn try_from(value: Todo) -> Result { + 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())?, + }) + } +} + +impl From for Todo { + fn from(value: McpTodo) -> Self { + let mut todo = Todo::new(); + + if let Some(summary) = value.summary { + todo.summary(summary.as_str()); + } + + if let Some(description) = value.description { + todo.description(description.as_str()); + } + + if let Some(start) = value.start { + todo.starts(start.to_utc()); + } + + if let Some(due) = value.due { + todo.due(due.to_utc()); + } + + todo + } +} \ No newline at end of file diff --git a/mcp_server_collection/src/config.rs b/mcp_server_collection/src/config.rs index aa24241..26aadc2 100644 --- a/mcp_server_collection/src/config.rs +++ b/mcp_server_collection/src/config.rs @@ -1,7 +1,8 @@ -use serde::{Deserialize, Serialize}; +use crate::McpServiceType; use own_assist_common::config_from_file; use own_assist_common::config_loader::ConfigLoadingError; -use crate::McpServiceType; +use serde::{Deserialize, Serialize}; +use toml::map::Map; #[derive(Debug, Deserialize, Serialize)] pub(crate) struct Config { @@ -13,11 +14,13 @@ pub(crate) struct Config { #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct ServerConfig { pub(crate) path: String, - pub(crate) r#type: McpServiceType + pub(crate) r#type: McpServiceType, + #[serde(rename = "additional-properties")] + pub(crate) additional_properties: Map, } impl Config { pub fn from_file() -> Result { config_from_file("server.toml") } -} \ No newline at end of file +} diff --git a/mcp_server_collection/src/datetime.rs b/mcp_server_collection/src/datetime.rs index 5563136..6a72405 100644 --- a/mcp_server_collection/src/datetime.rs +++ b/mcp_server_collection/src/datetime.rs @@ -1,38 +1,56 @@ -use chrono::{Datelike, Local}; -use rmcp::model::{Implementation, ServerCapabilities, ServerInfo}; -use rmcp::{tool, tool_handler, tool_router, ServerHandler}; +use crate::server_handler::{McpServerHandler, McpServerHandlerError}; +use chrono::{Datelike, Local, Utc}; use rmcp::handler::server::tool::ToolRouter; -use crate::McpServerHandler; +use rmcp::model::{Implementation, ServerCapabilities, ServerInfo}; +use rmcp::{ServerHandler, tool, tool_handler, tool_router}; +use toml::Value; +use toml::map::Map; #[derive(Debug, Clone)] -pub struct DateTimeHandler { - tool_router: ToolRouter +pub(crate) struct DateTimeHandler { + tool_router: ToolRouter, } impl McpServerHandler for DateTimeHandler { - fn new() -> Self { - DateTimeHandler { tool_router: Self::tool_router() } + async fn new( + _additional_properties: Map, + ) -> Result { + Ok(DateTimeHandler { + tool_router: Self::tool_router(), + }) } } #[tool_router] impl DateTimeHandler { - #[tool(description = "returns the local datetime in ISO 8601", annotations(read_only_hint = true))] + #[tool( + description = "returns the local datetime in ISO 8601", + annotations(read_only_hint = true) + )] fn get_local_datetime() -> String { Local::now().format("%+").to_string() } - #[tool(description = "returns the utc datetime in ISO 8601", annotations(read_only_hint = true))] + #[tool( + description = "returns the utc datetime in ISO 8601", + annotations(read_only_hint = true) + )] fn get_utc_datetime() -> String { - Local::now().format("%+").to_string() + Utc::now().format("%+").to_string() } - #[tool(description = "return current week number in year", annotations(read_only_hint = true))] + #[tool( + description = "return current week number in year", + annotations(read_only_hint = true) + )] fn get_week() -> String { Local::now().iso_week().week().to_string() } - #[tool(description = "return current weekday", annotations(read_only_hint = true))] + #[tool( + description = "return current weekday", + annotations(read_only_hint = true) + )] fn get_weekday() -> String { Local::now().weekday().to_string() } @@ -42,6 +60,10 @@ impl DateTimeHandler { impl ServerHandler for DateTimeHandler { fn get_info(&self) -> ServerInfo { ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) - .with_instructions("A simple clock/datetime provider. It is read only and thus safe to use.".to_string()).with_server_info(Implementation::new("datetime", env!("CARGO_PKG_VERSION"))) + .with_instructions( + "A simple clock/datetime provider. It is read only and thus safe to use." + .to_string(), + ) + .with_server_info(Implementation::new("datetime", env!("CARGO_PKG_VERSION"))) } -} \ No newline at end of file +} diff --git a/mcp_server_collection/src/main.rs b/mcp_server_collection/src/main.rs index cf01a31..24bf76b 100644 --- a/mcp_server_collection/src/main.rs +++ b/mcp_server_collection/src/main.rs @@ -1,51 +1,57 @@ +mod config; #[cfg(feature = "datetime")] pub mod datetime; -mod config; -use axum::response::Json; +#[cfg(feature = "caldav")] +pub mod caldav; +mod server_handler; + +use crate::caldav::CalDavHandler; +use crate::config::{Config, ServerConfig}; +#[cfg(feature = "datetime")] +use crate::datetime::DateTimeHandler; +use crate::server_handler::{McpServerHandler, McpServerHandlerError}; use axum::Router; -use rmcp::transport::{ - StreamableHttpServerConfig, - streamable_http_server::{session::local::LocalSessionManager, tower::StreamableHttpService}, -}; +use axum::response::Json; use serde::{Deserialize, Serialize}; use tokio::main; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use crate::config::{Config, ServerConfig}; -#[cfg(feature = "datetime")] -use crate::datetime::DateTimeHandler; - -pub trait McpServerHandler: rmcp::ServerHandler { - fn new() -> Self; - - fn mcp_service() -> StreamableHttpService { - StreamableHttpService::new(|| Ok(Self::new()), LocalSessionManager::default().into(), StreamableHttpServerConfig::default()) - } -} #[derive(Debug, Clone, Deserialize, Serialize)] #[non_exhaustive] pub enum McpServiceType { - DateTime + DateTime, + CalDav, } -pub(crate) fn mcp_router(server_configs: &Vec) -> Router { +pub(crate) async fn mcp_router( + server_configs: &Vec, +) -> Result { let mut router = Router::new(); for config in server_configs { router = match config.r#type { - McpServiceType::DateTime => { - router.route_service(config.path.as_str(), DateTimeHandler::mcp_service()) - } + McpServiceType::DateTime => router.route_service( + config.path.as_str(), + DateTimeHandler::mcp_service(config.additional_properties.clone()).await?, + ), + McpServiceType::CalDav => router.route_service( + config.path.as_str(), + CalDavHandler::mcp_service(config.additional_properties.clone()).await?, + ), } } - router + Ok(router) } fn bind_address_format(url: url::Url) -> String { - format!("{}:{}", url.host_str().expect("No host specified"), url.port().unwrap_or(8000)) + format!( + "{}:{}", + url.host_str().expect("No host specified"), + url.port().unwrap_or(8000) + ) } #[main] @@ -58,22 +64,43 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); - let config = Config::from_file().inspect_err(|e|{ - eprintln!("Error loading config: {e}"); - std::process::exit(1); - }).unwrap(); + let config = Config::from_file() + .inspect_err(|e| { + eprintln!("Error loading config: {e}"); + std::process::exit(1); + }) + .unwrap(); - println!("config: {config:#?}"); + let routes = Json( + config + .servers + .clone() + .iter() + .map(|server| server.path.clone()) + .collect::>(), + ); - let router = mcp_router(&config.servers).route("/", axum::routing::get(|| async { Json(config.servers) })); + let router = mcp_router(&config.servers) + .await + .inspect_err(|e| { + eprintln!("Error loading config: {e}"); + std::process::exit(1); + }) + .unwrap() + .route("/", axum::routing::get(|| async { routes })); - let bind_address = config.bind_address.unwrap_or(url::Url::parse("http://localhost:8000").unwrap()); + let bind_address = config + .bind_address + .unwrap_or(url::Url::parse("http://localhost:8000").unwrap()); println!("binding address at {bind_address}"); - let tcp_listener = tokio::net::TcpListener::bind(bind_address_format(bind_address)).await.inspect_err(|e|{ - eprintln!("Error bind tcp listener: {e:#?}"); - std::process::exit(1); - }).unwrap(); + let tcp_listener = tokio::net::TcpListener::bind(bind_address_format(bind_address)) + .await + .inspect_err(|e| { + eprintln!("Error bind tcp listener: {e:#?}"); + std::process::exit(1); + }) + .unwrap(); let ct = tokio_util::sync::CancellationToken::new(); @@ -83,4 +110,4 @@ async fn main() { ct.cancel(); }) .await; -} \ No newline at end of file +} diff --git a/mcp_server_collection/src/server_handler.rs b/mcp_server_collection/src/server_handler.rs new file mode 100644 index 0000000..9eee7f1 --- /dev/null +++ b/mcp_server_collection/src/server_handler.rs @@ -0,0 +1,55 @@ +use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; +use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum McpServerHandlerError { + #[error("Could not find property `{name}`")] + PropertyMissingError { name: String }, + #[error("Property is of wrong type: `{value}` could not be parsed")] + WrongPropertyType { value: String }, + #[error("Could not parse property `{value}`")] + PropertyParseError { value: String }, + #[error("internal error: {0}")] + InternalError(String), +} + +pub(crate) trait McpServerHandler: rmcp::ServerHandler + Clone { + fn new( + additional_properties: toml::map::Map, + ) -> impl Future>; + + async fn mcp_service( + additional_properties: toml::map::Map, + ) -> Result, McpServerHandlerError> { + let handler = Self::new(additional_properties).await?; + + Ok(StreamableHttpService::new( + move || Ok(handler.clone()), + LocalSessionManager::default().into(), + StreamableHttpServerConfig::default(), + )) + } +} + +pub(crate) fn get_additional_property<'a>( + key: &'a str, + additional_properties: &'a toml::map::Map, +) -> Result<&'a toml::Value, McpServerHandlerError> { + additional_properties.get::(key.as_ref()).ok_or( + McpServerHandlerError::PropertyMissingError { + name: key.to_string(), + }, + ) +} + +pub(crate) fn get_property_as_string<'a>( + key: &'a str, + additional_properties: &'a toml::map::Map, +) -> Result<&'a str, McpServerHandlerError> { + get_additional_property(key, additional_properties)? + .as_str() + .ok_or(McpServerHandlerError::PropertyMissingError { + name: key.to_string(), + }) +} diff --git a/own_mcp/src/mcp/chat.rs b/own_mcp/src/mcp/chat.rs index c8fe9da..1101cdb 100644 --- a/own_mcp/src/mcp/chat.rs +++ b/own_mcp/src/mcp/chat.rs @@ -221,19 +221,24 @@ impl AgentChat { log::debug!("calling tool {}", tool_call.function.name); let result = self.call_tool(tool_call, &mut permission_request_callback).await?; - let contents = match result { - Some(result) => result.content - .iter() - .filter_map(|content| content.as_text()) - .map(|text_content| text_content.text.clone()) - .collect::>(), - None => vec![String::from("Tool Permission Denied by the user")], - }; + // serialize structured content if it exists + if let Some(structured_content) = result.clone().and_then(|result|result.structured_content) { + self.message_history.push(ChatMessage::tool(structured_content.to_string())); + } else { + let contents = match result { + Some(result) => result.content + .iter() + .filter_map(|content| content.as_text()) + .map(|text_content| text_content.text.clone()) + .collect::>(), + None => vec![String::from("Tool Permission Denied by the user")], + }; - log::debug!("contents: {:#?}", contents); + log::debug!("contents: {:#?}", contents); - self.message_history - .push(ChatMessage::tool(contents.join("\n"))); + self.message_history + .push(ChatMessage::tool(contents.join("\n"))); + } } response = self diff --git a/src/main.rs b/src/main.rs index 0d7fd39..1f2fc0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,12 +81,6 @@ async fn main() { }) .unwrap(); - /*let audio_client = config.audio_client(); - log::debug!("Audio server status {:?}", audio_client.status().await.inspect_err(|e|{ - log::error!("audio server error {}", e); - std::process::exit(1); - }).unwrap());*/ - let ollama = config.ollama_instance(); let model_name = &config.ollama_config().model.name;