diff --git a/Cargo.lock b/Cargo.lock index 55d3e248c182..850ff8496929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "const-random", "getrandom 0.2.15", "once_cell", "serde", @@ -116,6 +115,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width 0.1.14", + "yansi-term", +] + [[package]] name = "ansi_colours" version = "1.2.3" @@ -220,255 +229,143 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "arrow" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05048a8932648b63f21c37d88b552ccc8a65afb6dfe9fc9f30ce79174c2e7a85" -dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-csv", - "arrow-data", - "arrow-ipc", - "arrow-json", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", - "arrow-string", -] - -[[package]] -name = "arrow-arith" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8a57966e43bfe9a3277984a14c24ec617ad874e4c0e1d2a1b083a39cfbf22c" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "num", -] - -[[package]] -name = "arrow-array" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f4a9468c882dc66862cef4e1fd8423d47e67972377d85d80e022786427768c" -dependencies = [ - "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "hashbrown 0.14.5", - "num", -] - -[[package]] -name = "arrow-buffer" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c975484888fc95ec4a632cdc98be39c085b1bb518531b0c80c5d462063e5daa1" -dependencies = [ - "bytes", - "half", - "num", -] - -[[package]] -name = "arrow-cast" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da26719e76b81d8bc3faad1d4dbdc1bcc10d14704e63dc17fc9f3e7e1e567c8e" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "atoi", - "base64 0.22.1", - "chrono", - "half", - "lexical-core", - "num", - "ryu", -] - -[[package]] -name = "arrow-csv" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13c36dc5ddf8c128df19bab27898eea64bf9da2b555ec1cd17a8ff57fba9ec2" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "chrono", - "csv", - "csv-core", - "lazy_static", - "lexical-core", - "regex", -] - -[[package]] -name = "arrow-data" -version = "52.2.0" +name = "assert-json-diff" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd9d6f18c65ef7a2573ab498c374d8ae364b4a4edf67105357491c031f716ca5" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" dependencies = [ - "arrow-buffer", - "arrow-schema", - "half", - "num", + "serde", + "serde_json", ] [[package]] -name = "arrow-ipc" -version = "52.2.0" +name = "async-broadcast" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e786e1cdd952205d9a8afc69397b317cfbb6e0095e445c69cda7e8da5c1eeb0f" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "flatbuffers", + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] -name = "arrow-json" -version = "52.2.0" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb22284c5a2a01d73cebfd88a33511a3234ab45d66086b2ca2d1228c3498e445" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "indexmap 2.7.1", - "lexical-core", - "num", - "serde", - "serde_json", + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] -name = "arrow-ord" -version = "52.2.0" +name = "async-compression" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42745f86b1ab99ef96d1c0bcf49180848a64fe2c7a7a0d945bc64fa2b21ba9bc" +checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "half", - "num", + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", ] [[package]] -name = "arrow-row" -version = "52.2.0" +name = "async-executor" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd09a518c602a55bd406bcc291a967b284cfa7a63edfbf8b897ea4748aad23c" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "half", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", ] [[package]] -name = "arrow-schema" -version = "52.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e972cd1ff4a4ccd22f86d3e53e835c2ed92e0eea6a3e8eadb72b4f1ac802cf8" - -[[package]] -name = "arrow-select" -version = "52.2.0" +name = "async-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600bae05d43483d216fb3494f8c32fdbefd8aa4e1de237e790dbb3d9f44690a3" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "num", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.7", + "slab", + "windows-sys 0.61.0", ] [[package]] -name = "arrow-string" -version = "52.2.0" +name = "async-lock" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc1985b67cb45f6606a248ac2b4a288849f196bab8c657ea5589f47cdd55e6" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "memchr", - "num", - "regex", - "regex-syntax 0.8.5", + "event-listener", + "event-listener-strategy", + "pin-project-lite", ] [[package]] -name = "assert-json-diff" -version = "2.0.2" +name = "async-process" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "serde", - "serde_json", + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.7", ] [[package]] -name = "async-broadcast" -version = "0.7.2" +name = "async-recursion" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", + "proc-macro2", + "quote", + "syn 2.0.99", ] [[package]] -name = "async-compression" -version = "0.4.20" +name = "async-signal" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ - "brotli", - "flate2", + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "zstd", - "zstd-safe", + "futures-io", + "rustix 1.0.7", + "signal-hook-registry", + "slab", + "windows-sys 0.61.0", ] [[package]] @@ -493,6 +390,12 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.87" @@ -504,15 +407,6 @@ dependencies = [ "syn 2.0.99", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -1134,6 +1028,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "annotate-snippets", + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.99", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1222,6 +1137,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borrow-or-share" version = "0.2.2" @@ -1294,6 +1231,20 @@ name = "bytemuck" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] [[package]] name = "byteorder" @@ -1361,6 +1312,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.10.0" @@ -1446,6 +1406,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.44" @@ -1659,6 +1630,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "cookie_store" version = "0.21.1" @@ -1703,30 +1683,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", - "core-graphics-types", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1840,41 +1796,20 @@ dependencies = [ ] [[package]] -name = "csv" -version = "1.3.1" +name = "ctor" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", + "quote", + "syn 2.0.99", ] [[package]] -name = "csv-core" -version = "0.1.12" +name = "darling" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.99", -] - -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core 0.20.10", "darling_macro 0.20.10", @@ -2066,6 +2001,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2077,6 +2024,15 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -2128,6 +2084,52 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.9.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -2164,12 +2166,39 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -2338,16 +2367,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "flatbuffers" -version = "24.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" -dependencies = [ - "bitflags 1.3.2", - "rustc_version", -] - [[package]] name = "flate2" version = "1.1.0" @@ -2387,28 +2406,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.99", + "foreign-types-shared", ] [[package]] @@ -2417,12 +2415,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2506,6 +2498,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2547,6 +2552,30 @@ dependencies = [ "slab", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.9.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2631,6 +2660,26 @@ dependencies = [ "url", ] +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.2" @@ -2656,7 +2705,6 @@ version = "1.8.0" dependencies = [ "ahash", "anyhow", - "arrow", "async-stream", "async-trait", "aws-config", @@ -2774,7 +2822,6 @@ dependencies = [ "jsonschema", "mcp-client", "mcp-core", - "mcp-server", "nix 0.30.1", "once_cell", "rand 0.8.5", @@ -2826,7 +2873,6 @@ dependencies = [ "lopdf", "lru", "mcp-core", - "mcp-server", "mpatch", "oauth2", "once_cell", @@ -2883,7 +2929,6 @@ dependencies = [ "goose-mcp", "http 1.2.0", "mcp-core", - "mcp-server", "reqwest 0.12.12", "rmcp", "schemars", @@ -2972,7 +3017,6 @@ checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", - "num-traits", ] [[package]] @@ -3787,80 +3831,38 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - -[[package]] -name = "lexical-core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" -dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", -] - -[[package]] -name = "lexical-parse-float" -version = "0.8.5" +name = "khronos-egl" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ - "lexical-parse-integer", - "lexical-util", - "static_assertions", + "libc", + "pkg-config", ] [[package]] -name = "lexical-parse-integer" -version = "0.8.6" +name = "khronos_api" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" -dependencies = [ - "lexical-util", - "static_assertions", -] +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] -name = "lexical-util" -version = "0.8.5" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" -dependencies = [ - "static_assertions", -] +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lexical-write-float" -version = "0.8.5" +name = "lazycell" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" -dependencies = [ - "lexical-util", - "lexical-write-integer", - "static_assertions", -] +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "lexical-write-integer" -version = "0.8.5" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" -dependencies = [ - "lexical-util", - "static_assertions", -] +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" @@ -3901,10 +3903,14 @@ dependencies = [ ] [[package]] -name = "libm" -version = "0.2.15" +name = "libloading" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] [[package]] name = "libredox" @@ -3917,6 +3923,55 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libspa" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +dependencies = [ + "bitflags 2.9.0", + "cc", + "convert_case", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.27.1", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + +[[package]] +name = "libwayshot-xcap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22" +dependencies = [ + "drm", + "gbm", + "gl", + "image 0.25.5", + "khronos-egl", + "memmap2", + "rustix 1.0.7", + "thiserror 2.0.12", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "libz-sys" version = "1.1.21" @@ -3941,6 +3996,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -4106,26 +4167,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "mcp-server" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures", - "mcp-core", - "pin-project", - "rmcp", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tower 0.4.13", - "tower-service", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - [[package]] name = "md-5" version = "0.10.6" @@ -4142,12 +4183,30 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + [[package]] name = "memo-map" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -4297,6 +4356,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -4319,6 +4389,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -4470,7 +4541,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -4527,6 +4597,168 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-av-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e085a2e16c61dadbad7a808fc9d5b5f8472b1b825b53d529c9f64ccac78e722" +dependencies = [ + "bitflags 2.9.0", + "block2", + "dispatch2", + "objc2", + "objc2-avf-audio", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +dependencies = [ + "bitflags 2.9.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.0", + "objc2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +dependencies = [ + "bitflags 2.9.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.0", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.0", + "block2", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", + "objc2-metal", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7afa6822e2fa20dfc88d10186b2432bf8560b5ed73ec9d31efd78277bc878" +dependencies = [ + "bitflags 2.9.0", + "block2", + "dispatch2", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" +dependencies = [ + "bitflags 2.9.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -4540,7 +4772,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.0", + "block2", + "libc", "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.9.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" +dependencies = [ + "bitflags 2.9.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +dependencies = [ + "bitflags 2.9.0", + "objc2", + "objc2-foundation", ] [[package]] @@ -4594,7 +4862,7 @@ checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags 2.9.0", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "once_cell", "openssl-macros", @@ -4737,6 +5005,16 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "outref" version = "0.5.2" @@ -4890,7 +5168,46 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pipewire" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "libc", + "libspa", + "libspa-sys", + "nix 0.27.1", + "once_cell", + "pipewire-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pipewire-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] [[package]] name = "pkg-config" @@ -4952,6 +5269,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.0.7", + "windows-sys 0.61.0", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -5008,6 +5339,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -5892,6 +6232,12 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6016,6 +6362,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -7149,6 +7506,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "umya-spreadsheet" version = "2.2.3" @@ -7481,6 +7849,94 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.7", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.0", + "rustix 1.0.7", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.2", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbd4f3aba6c9fba70445ad2a484c0ef0356c1a9459b1e8e435bedc1971a6222" +dependencies = [ + "bitflags 2.9.0", + "downcast-rs", + "rustix 1.0.7", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "libc", + "log", + "memoffset", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -7562,6 +8018,12 @@ dependencies = [ "winsafe", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "wild" version = "2.2.1" @@ -7614,22 +8076,34 @@ dependencies = [ [[package]] name = "windows" -version = "0.58.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-core 0.59.0", + "windows-targets 0.53.3", ] [[package]] name = "windows" -version = "0.59.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.3", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -7655,46 +8129,46 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.58.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-implement 0.59.0", + "windows-interface 0.59.1", + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.3", ] [[package]] name = "windows-core" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.59.0", + "windows-implement 0.60.0", "windows-interface 0.59.1", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.3", + "windows-strings 0.4.2", ] [[package]] -name = "windows-implement" -version = "0.57.0" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.99", + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", @@ -7713,10 +8187,10 @@ dependencies = [ ] [[package]] -name = "windows-interface" -version = "0.57.0" +name = "windows-implement" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -7725,9 +8199,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", @@ -7751,6 +8225,22 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.2.0" @@ -7786,7 +8276,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -7805,7 +8295,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -7844,6 +8343,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -7896,7 +8413,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7907,6 +8424,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8169,20 +8695,34 @@ dependencies = [ [[package]] name = "xcap" -version = "0.0.14" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1107223d8283abdd9f22bad27cf36562ef7d3941d82360c75c303656b7dfcb66" +checksum = "604b4ac2b821a6c37510b9a065a1d52cf364375211a3d9a1557105ad8522cac6" dependencies = [ - "core-foundation 0.10.0", - "core-graphics", - "dbus", + "dispatch2", "image 0.25.5", + "lazy_static", + "libwayshot-xcap", "log", + "objc2", + "objc2-app-kit", + "objc2-av-foundation", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation", "percent-encoding", - "sysinfo 0.32.1", - "thiserror 1.0.69", - "windows 0.58.0", + "pipewire", + "rand 0.9.1", + "scopeguard", + "serde", + "thiserror 2.0.12", + "url", + "widestring", + "windows 0.61.3", "xcb", + "zbus", ] [[package]] @@ -8228,6 +8768,15 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] + [[package]] name = "yoke" version = "0.7.5" @@ -8252,6 +8801,66 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "windows-sys 0.60.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.99", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -8428,3 +9037,43 @@ checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.99", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.99", + "winnow", +] diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index cd76ec44cd19..19dc22ad4898 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -19,7 +19,6 @@ goose = { path = "../goose" } goose-bench = { path = "../goose-bench" } goose-mcp = { path = "../goose-mcp" } mcp-client = { path = "../mcp-client" } -mcp-server = { path = "../mcp-server" } mcp-core = { path = "../mcp-core" } rmcp = { workspace = true } agent-client-protocol = "0.1.1" diff --git a/crates/goose-cli/src/commands/mcp.rs b/crates/goose-cli/src/commands/mcp.rs index a00150a1b75c..6efb7708f4c5 100644 --- a/crates/goose-cli/src/commands/mcp.rs +++ b/crates/goose-cli/src/commands/mcp.rs @@ -1,31 +1,12 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use goose_mcp::{ - AutoVisualiserRouter, ComputerControllerRouter, DeveloperServer, MemoryRouter, TutorialRouter, + AutoVisualiserServer, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, }; -use mcp_server::router::RouterService; -use mcp_server::{BoundedService, ByteTransport, Server}; use rmcp::{transport::stdio, ServiceExt}; -use tokio::io::{stdin, stdout}; - -use std::sync::Arc; -use tokio::sync::Notify; - -#[cfg(unix)] -use nix::sys::signal::{kill, Signal}; -#[cfg(unix)] -use nix::unistd::getpgrp; -#[cfg(unix)] -use nix::unistd::Pid; pub async fn run_server(name: &str) -> Result<()> { crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; - if name == "googledrive" || name == "google_drive" { - return Err(anyhow!( - "the built-in Google Drive extension has been removed" - )); - } - tracing::info!("Starting MCP server"); if name == "developer" { @@ -39,44 +20,47 @@ pub async fn run_server(name: &str) -> Result<()> { service.waiting().await?; return Ok(()); } + if name == "tutorial" { + let service = TutorialServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; - let router: Option> = match name { - "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - "autovisualiser" => Some(Box::new(RouterService(AutoVisualiserRouter::new()))), - "memory" => Some(Box::new(RouterService(MemoryRouter::new()))), - "tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))), - _ => None, - }; - - let shutdown = Arc::new(Notify::new()); - let shutdown_clone = shutdown.clone(); - - tokio::spawn(async move { - crate::signal::shutdown_signal().await; - shutdown_clone.notify_one(); - }); + service.waiting().await?; + return Ok(()); + } + if name == "memory" { + let service = MemoryServer::new().serve(stdio()).await.inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; - let server = Server::new(router.unwrap_or_else(|| panic!("Unknown server requested {}", name))); - let transport = ByteTransport::new(stdin(), stdout()); + service.waiting().await?; + return Ok(()); + } + if name == "computercontroller" { + let service = ComputerControllerServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; - tracing::info!("Server initialized and ready to handle requests"); + service.waiting().await?; + return Ok(()); + } + if name == "autovisualiser" { + let service = AutoVisualiserServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; - tokio::select! { - result = server.run(transport) => { - Ok(result?) - } - _ = shutdown.notified() => { - // On Unix systems, kill the entire process group - #[cfg(unix)] - { - fn terminate_process_group() { - let pgid = getpgrp(); - kill(Pid::from_raw(-pgid.as_raw()), Signal::SIGTERM) - .expect("Failed to send SIGTERM to process group"); - } - terminate_process_group(); - } - Ok(()) - } + service.waiting().await?; + return Ok(()); } + + panic!("Unknown server requested {}", name); } diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 9480e73c82b2..0ed31ae57de5 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -12,7 +12,6 @@ workspace = true [dependencies] mcp-core = { path = "../mcp-core" } -mcp-server = { path = "../mcp-server" } rmcp = { version = "0.6.0", features = ["server", "client", "transport-io", "macros"] } anyhow = "1.0.94" tokio = { version = "1", features = ["full"] } @@ -29,7 +28,7 @@ schemars = "1.0" lazy_static = "1.5" shellexpand = "3.1.0" indoc = "2.0.5" -xcap = "0.0.14" +xcap = "0.7.0" reqwest = { version = "0.11", features = [ "json", "rustls-tls-native-roots", diff --git a/crates/goose-mcp/examples/mcp.rs b/crates/goose-mcp/examples/mcp.rs index 052e78572672..35e3057c9ec8 100644 --- a/crates/goose-mcp/examples/mcp.rs +++ b/crates/goose-mcp/examples/mcp.rs @@ -1,9 +1,7 @@ -// An example script to run an MCP server +// An example script to run an MCP server using the rmcp SDK use anyhow::Result; -use goose_mcp::MemoryRouter; -use mcp_server::router::RouterService; -use mcp_server::{ByteTransport, Server}; -use tokio::io::{stdin, stdout}; +use goose_mcp::MemoryServer; +use rmcp::{transport::stdio, ServiceExt}; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{self, EnvFilter}; @@ -22,15 +20,14 @@ async fn main() -> Result<()> { .with_line_number(true) .init(); - tracing::info!("Starting MCP server"); + tracing::info!("Starting MCP server using rmcp SDK"); - // Create an instance of our counter router - let router = RouterService(MemoryRouter::new()); + // Create an instance of our memory server + let memory_server = MemoryServer::new(); - // Create and run the server - let server = Server::new(router); - let transport = ByteTransport::new(stdin(), stdout()); + // Create the transport and run the server + let (stdin, stdout) = stdio(); + memory_server.serve((stdin, stdout)).await?; - tracing::info!("Server initialized and ready to handle requests"); - Ok(server.run(transport).await?) + Ok(()) } diff --git a/crates/goose-mcp/src/autovisualiser/mod.rs b/crates/goose-mcp/src/autovisualiser/mod.rs index 896340177f39..4ba269fe41e7 100644 --- a/crates/goose-mcp/src/autovisualiser/mod.rs +++ b/crates/goose-mcp/src/autovisualiser/mod.rs @@ -1,564 +1,324 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; use etcetera::{choose_app_strategy, AppStrategy}; -use indoc::{formatdoc, indoc}; +use indoc::formatdoc; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::{ + handler::server::router::tool::ToolRouter, + model::{ + CallToolResult, Content, ErrorCode, ErrorData, Implementation, Role, ServerCapabilities, + ServerInfo, + }, + schemars::{self, JsonSchema}, + tool, tool_handler, tool_router, ServerHandler, +}; +use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex}; -use tokio::sync::mpsc; +use std::path::PathBuf; + +// Sankey diagram structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct SankeyNode { + /// Name of the node (required) + pub name: String, + /// Optional category for grouping/coloring + #[serde(skip_serializing_if = "Option::is_none")] + pub category: Option, +} -use mcp_core::{ - handler::{PromptError, ResourceError}, - protocol::ServerCapabilities, -}; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::{ - Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, Resource, ResourceContents, Role, Tool, -}; -use rmcp::object; - -/// Validates that the data parameter is a proper JSON value and not a string -fn validate_data_param(params: &Value, allow_array: bool) -> Result { - let data_value = params.get("data").ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'data' parameter".to_string(), - None, - ) - })?; - - if data_value.is_string() { - return Err(ErrorData::new( - ErrorCode::INVALID_PARAMS, - "The 'data' parameter must be a JSON object, not a JSON string. Please provide valid JSON without comments.".to_string(), - None, - )); - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct SankeyLink { + /// Source node name + pub source: String, + /// Target node name + pub target: String, + /// Flow value between nodes + pub value: f64, +} - if allow_array { - if !data_value.is_object() && !data_value.is_array() { - return Err(ErrorData::new( - ErrorCode::INVALID_PARAMS, - "The 'data' parameter must be a JSON object or array.".to_string(), - None, - )); - } - } else if !data_value.is_object() { - return Err(ErrorData::new( - ErrorCode::INVALID_PARAMS, - "The 'data' parameter must be a JSON object.".to_string(), - None, - )); - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct SankeyData { + /// Array of nodes in the diagram + pub nodes: Vec, + /// Array of links between nodes + pub links: Vec, +} - Ok(data_value.clone()) +/// Parameters for the render_sankey tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderSankeyParams { + /// Sankey diagram data containing nodes and links + pub data: SankeyData, } -/// An extension for automatic data visualization and UI generation -#[derive(Clone)] -pub struct AutoVisualiserRouter { - tools: Vec, - #[allow(dead_code)] - cache_dir: PathBuf, - active_resources: Arc>>, - instructions: String, +// Radar chart structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RadarDataset { + /// Label for this dataset + pub label: String, + /// Data values for each dimension + pub data: Vec, } -impl Default for AutoVisualiserRouter { - fn default() -> Self { - Self::new() - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RadarData { + /// Array of labels for each dimension/axis + pub labels: Vec, + /// Array of datasets to compare + pub datasets: Vec, } -impl AutoVisualiserRouter { - fn create_sankey_tool() -> Tool { - Tool::new( - "render_sankey", - indoc! {r#" - show a Sankey diagram from flow data - The data must contain: - - nodes: Array of objects with 'name' and optional 'category' properties - - links: Array of objects with 'source', 'target', and 'value' properties - - Example: - { - "nodes": [ - {"name": "Source A", "category": "source"}, - {"name": "Target B", "category": "target"} - ], - "links": [ - {"source": "Source A", "target": "Target B", "value": 100} - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["nodes", "links"], - "properties": { - "nodes": { - "type": "array", - "items": { - "type": "object", - "required": ["name"], - "properties": { - "name": {"type": "string"}, - "category": {"type": "string"} - } - } - }, - "links": { - "type": "array", - "items": { - "type": "object", - "required": ["source", "target", "value"], - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "value": {"type": "number"} - } - } - } - } - } - } - }), - ) - } +/// Parameters for the render_radar tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderRadarParams { + /// Radar chart data containing labels and datasets + pub data: RadarData, +} - fn create_radar_tool() -> Tool { - Tool::new( - "render_radar", - indoc! {r#" - show a radar chart (spider chart) for multi-dimensional data comparison - - The data must contain: - - labels: Array of strings representing the dimensions/axes - - datasets: Array of dataset objects with 'label' and 'data' properties - - Example: - { - "labels": ["Speed", "Strength", "Endurance", "Agility", "Intelligence"], - "datasets": [ - { - "label": "Player 1", - "data": [85, 70, 90, 75, 80] - }, - { - "label": "Player 2", - "data": [75, 85, 80, 90, 70] - } - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["labels", "datasets"], - "properties": { - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "datasets": { - "type": "array", - "items": { - "type": "object", - "required": ["label", "data"], - "properties": { - "label": {"type": "string"}, - "data": { - "type": "array", - "items": {"type": "number"} - } - } - } - } - } - } - } - }), - ) - } +// Donut/Pie chart structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum DonutDataItem { + /// Simple numeric value + Number(f64), + /// Labeled value object + LabeledValue { label: String, value: f64 }, +} - fn create_donut_tool() -> Tool { - Tool::new( - "render_donut", - indoc! {r#" - show pie or donut charts for categorical data visualization - Supports single or multiple charts in a grid layout. - - Each chart should contain: - - data: Array of values or objects with 'label' and 'value' - - type: Optional 'doughnut' (default) or 'pie' - - title: Optional chart title - - labels: Optional array of labels (if data is just numbers) - - Example single chart: - { - "title": "Budget", - "type": "doughnut", - "data": [ - {"label": "Marketing", "value": 25000}, - {"label": "Development", "value": 35000} - ] - } - - Example multiple charts: - [{ - "title": "Q1 Sales", - "labels": ["Product A", "Product B"], - "data": [45000, 38000] - }] - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "oneOf": [ - { - "type": "object", - "properties": { - "title": {"type": "string"}, - "type": {"type": "string", "enum": ["doughnut", "pie"]}, - "labels": { - "type": "array", - "items": {"type": "string"} - }, - "data": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number"}, - { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": {"type": "string"}, - "value": {"type": "number"} - } - } - ] - } - } - }, - "required": ["data"] - }, - { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "type": {"type": "string", "enum": ["doughnut", "pie"]}, - "labels": { - "type": "array", - "items": {"type": "string"} - }, - "data": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number"}, - { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": {"type": "string"}, - "value": {"type": "number"} - } - } - ] - } - } - }, - "required": ["data"] - } - } - ] - } - } - }), - ) - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ChartType { + Doughnut, + Pie, +} - fn create_treemap_tool() -> Tool { - Tool::new( - "render_treemap", - indoc! {r#" - show a treemap visualization for hierarchical data with proportional area representation as boxes - - The data should be a hierarchical structure with: - - name: Name of the node (required) - - value: Numeric value for leaf nodes (optional for parent nodes) - - children: Array of child nodes (optional) - - category: Category for coloring (optional) - - Example: - { - "name": "Root", - "children": [ - { - "name": "Group A", - "children": [ - {"name": "Item 1", "value": 100, "category": "Type1"}, - {"name": "Item 2", "value": 200, "category": "Type2"} - ] - }, - {"name": "Item 3", "value": 150, "category": "Type1"} - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["name"], - "properties": { - "name": {"type": "string"}, - "value": {"type": "number"}, - "category": {"type": "string"}, - "children": { - "type": "array", - "items": { - "$ref": "#/properties/data" - } - } - } - } - } - }), - ) - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct DonutChart { + /// Optional chart title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Chart type - 'doughnut' (default) or 'pie' + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub chart_type: Option, + /// Optional array of labels (if data is just numbers) + #[serde(skip_serializing_if = "Option::is_none")] + pub labels: Option>, + /// Array of values or labeled value objects + pub data: Vec, +} - fn create_chord_tool() -> Tool { - Tool::new( - "render_chord", - indoc! {r#" - Show a chord diagram visualization for showing relationships and flows between entities. - - The data must contain: - - labels: Array of strings representing the entities - - matrix: 2D array of numbers representing flows (matrix[i][j] = flow from i to j) - - Example: - { - "labels": ["North America", "Europe", "Asia", "Africa"], - "matrix": [ - [0, 15, 25, 8], - [18, 0, 20, 12], - [22, 18, 0, 15], - [5, 10, 18, 0] - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["labels", "matrix"], - "properties": { - "labels": { - "type": "array", - "items": {"type": "string"} - }, - "matrix": { - "type": "array", - "items": { - "type": "array", - "items": {"type": "number"} - } - } - } - } - } - }), - ) - } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum DonutData { + /// Single chart + Single(DonutChart), + /// Multiple charts for grid layout + Multiple(Vec), +} - fn create_map_tool() -> Tool { - Tool::new( - "render_map", - indoc! {r#" - show an interactive map visualization with location markers using Leaflet. - - The data must contain: - - markers: Array of objects with 'lat', 'lng', and optional properties - - title: Optional title for the map (default: "Interactive Map") - - subtitle: Optional subtitle (default: "Geographic data visualization") - - center: Optional center point {lat, lng} (default: USA center) - - zoom: Optional initial zoom level (default: 4) - - clustering: Optional boolean to enable/disable clustering (default: true) - - autoFit: Optional boolean to auto-fit map to markers (default: true) - - Marker properties: - - lat: Latitude (required) - - lng: Longitude (required) - - name: Location name - - value: Numeric value for sizing/coloring - - description: Description text - - popup: Custom popup HTML - - color: Custom marker color - - label: Custom marker label - - useDefaultIcon: Use default Leaflet icon - - Example: - { - "title": "Store Locations", - "markers": [ - {"lat": 37.7749, "lng": -122.4194, "name": "SF Store", "value": 150000}, - {"lat": 40.7128, "lng": -74.0060, "name": "NYC Store", "value": 200000} - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["markers"], - "properties": { - "markers": { - "type": "array", - "items": { - "type": "object", - "required": ["lat", "lng"], - "properties": { - "lat": {"type": "number"}, - "lng": {"type": "number"}, - "name": {"type": "string"}, - "value": {"type": "number"}, - "description": {"type": "string"}, - "popup": {"type": "string"}, - "color": {"type": "string"}, - "label": {"type": "string"}, - "useDefaultIcon": {"type": "boolean"} - } - } - }, - "title": {"type": "string"}, - "subtitle": {"type": "string"}, - "center": { - "type": "object", - "properties": { - "lat": {"type": "number"}, - "lng": {"type": "number"} - } - }, - "zoom": {"type": "number"}, - "clustering": {"type": "boolean"}, - "clusterRadius": {"type": "number"}, - "autoFit": {"type": "boolean"} - } - } - } - }), - ) +/// Parameters for the render_donut tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderDonutParams { + /// Donut/pie chart data - can be single chart or array of charts + pub data: DonutData, +} + +// Treemap structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct TreemapNode { + /// Name of the node (required) + pub name: String, + /// Numeric value for leaf nodes (optional for parent nodes) + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Category for coloring (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub category: Option, + /// Array of child nodes (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub children: Option>, +} + +/// Parameters for the render_treemap tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderTreemapParams { + /// Treemap data with hierarchical structure + pub data: TreemapNode, +} + +// Chord diagram structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ChordData { + /// Array of strings representing the entities + pub labels: Vec, + /// 2D array of numbers representing flows (matrix[i][j] = flow from i to j) + pub matrix: Vec>, +} + +/// Parameters for the render_chord tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderChordParams { + /// Chord diagram data containing labels and matrix + pub data: ChordData, +} + +// Map structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct MapMarker { + /// Latitude (required) + pub lat: f64, + /// Longitude (required) + pub lng: f64, + /// Location name + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Numeric value for sizing/coloring + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Description text + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Custom popup HTML + #[serde(skip_serializing_if = "Option::is_none")] + pub popup: Option, + /// Custom marker color + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + /// Custom marker label + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + /// Use default Leaflet icon + #[serde(skip_serializing_if = "Option::is_none")] + pub use_default_icon: Option, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct MapCenter { + pub lat: f64, + pub lng: f64, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct MapData { + /// Array of location markers + pub markers: Vec, + /// Optional title for the map (default: "Interactive Map") + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Optional subtitle (default: "Geographic data visualization") + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle: Option, + /// Optional center point {lat, lng} (default: USA center) + #[serde(skip_serializing_if = "Option::is_none")] + pub center: Option, + /// Optional initial zoom level (default: 4) + #[serde(skip_serializing_if = "Option::is_none")] + pub zoom: Option, + /// Optional boolean to enable/disable clustering (default: true) + #[serde(skip_serializing_if = "Option::is_none")] + pub clustering: Option, + /// Optional boolean to auto-fit map to markers (default: true) + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_fit: Option, +} + +/// Parameters for the render_map tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RenderMapParams { + /// Map data containing markers and optional configuration + pub data: MapData, +} + +// Chart structures +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ChartDataType { + Line, + Scatter, + Bar, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum ChartDataPoint { + /// Simple numeric value + Number(f64), + /// X,Y coordinate object + Coordinate { x: f64, y: f64 }, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ChartDataset { + /// Label for this dataset + pub label: String, + /// Data points for the chart + pub data: Vec, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ChartData { + /// Chart type: 'line', 'scatter', or 'bar' + #[serde(rename = "type")] + pub chart_type: ChartDataType, + /// Array of datasets to display + pub datasets: Vec, + /// Optional array of labels for x-axis (mainly for bar charts) + #[serde(skip_serializing_if = "Option::is_none")] + pub labels: Option>, + /// Optional chart title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Optional chart subtitle + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle: Option, + /// Optional x-axis label + #[serde(skip_serializing_if = "Option::is_none")] + pub x_axis_label: Option, + /// Optional y-axis label + #[serde(skip_serializing_if = "Option::is_none")] + pub y_axis_label: Option, + /// Optional chart configuration options + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +/// Parameters for the show_chart tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ShowChartParams { + /// Chart data containing type, datasets, and optional configuration + pub data: ChartData, +} + +#[derive(Debug)] +pub struct AutoVisualiserServer { + tool_router: ToolRouter, + instructions: String, + cache_dir: PathBuf, +} + +#[tool_handler(router = self.tool_router)] +impl ServerHandler for AutoVisualiserServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "goose-autovisualiser".to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + capabilities: ServerCapabilities::builder().enable_tools().build(), + instructions: Some(self.instructions.clone()), + ..Default::default() + } } +} - fn create_show_chart_tool() -> Tool { - Tool::new( - "show_chart", - indoc! {r#" - show interactive line, scatter, or bar charts - - Required: type ('line', 'scatter', or 'bar'), datasets array - Optional: labels, title, subtitle, xAxisLabel, yAxisLabel, options - - Example: - { - "type": "line", - "title": "Monthly Sales", - "labels": ["Jan", "Feb", "Mar"], - "datasets": [ - {"label": "Product A", "data": [65, 59, 80]} - ] - } - "#}, - object!({ - "type": "object", - "required": ["data"], - "properties": { - "data": { - "type": "object", - "required": ["type", "datasets"], - "properties": { - "type": { - "type": "string", - "enum": ["line", "scatter", "bar"] - }, - "title": {"type": "string"}, - "subtitle": {"type": "string"}, - "xAxisLabel": {"type": "string"}, - "yAxisLabel": {"type": "string"}, - "labels": { - "type": "array", - "items": {"type": "string"} - }, - "datasets": { - "type": "array", - "items": { - "type": "object", - "required": ["data"], - "properties": { - "label": {"type": "string"}, - "data": { - "oneOf": [ - { - "type": "array", - "items": {"type": "number"} - }, - { - "type": "array", - "items": { - "type": "object", - "required": ["x", "y"], - "properties": { - "x": {"type": "number"}, - "y": {"type": "number"} - } - } - } - ] - }, - "backgroundColor": {"type": "string"}, - "borderColor": {"type": "string"}, - "borderWidth": {"type": "number"}, - "tension": {"type": "number"}, - "fill": {"type": "boolean"} - } - } - }, - "options": {"type": "object"} - } - } - } - }), - ) +impl Default for AutoVisualiserServer { + fn default() -> Self { + Self::new() } +} +#[tool_router(router = tool_router)] +impl AutoVisualiserServer { pub fn new() -> Self { - let render_sankey_tool = Self::create_sankey_tool(); - let render_radar_tool = Self::create_radar_tool(); - let render_donut_tool = Self::create_donut_tool(); - let render_treemap_tool = Self::create_treemap_tool(); - let render_chord_tool = Self::create_chord_tool(); - let render_map_tool = Self::create_map_tool(); - let show_chart_tool = Self::create_show_chart_tool(); - // choose_app_strategy().cache_dir() // - macOS/Linux: ~/.cache/goose/autovisualiser/ // - Windows: ~\AppData\Local\Block\goose\cache\autovisualiser\ @@ -588,26 +348,57 @@ impl AutoVisualiserRouter { "#}; Self { - tools: vec![ - render_sankey_tool, - render_radar_tool, - render_donut_tool, - render_treemap_tool, - render_chord_tool, - render_map_tool, - show_chart_tool, - ], - cache_dir, - active_resources: Arc::new(Mutex::new(HashMap::new())), + tool_router: Self::tool_router(), instructions, + cache_dir, + } + } + + /// Validates that the data parameter is a proper JSON value and not a string + #[cfg(test)] + fn validate_data_param(data: &Value, allow_array: bool) -> Result { + if data.is_string() { + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "The 'data' parameter must be a JSON object, not a JSON string. Please provide valid JSON without comments.".to_string(), + None, + )); } + + if allow_array { + if !data.is_object() && !data.is_array() { + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "The 'data' parameter must be a JSON object or array.".to_string(), + None, + )); + } + } else if !data.is_object() { + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "The 'data' parameter must be a JSON object.".to_string(), + None, + )); + } + + Ok(data.clone()) } - async fn render_sankey(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show a Sankey diagram from flow data + #[tool( + name = "render_sankey", + description = "show a Sankey diagram from flow data. The data must contain: +nodes (Array of objects with 'name' and optional 'category' properties) and +links (Array of objects with 'source', 'target', and 'value' properties)" + )] + pub async fn render_sankey( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -638,23 +429,34 @@ impl AutoVisualiserRouter { let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { uri: "ui://sankey/diagram".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn render_radar(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show a radar chart (spider chart) for multi-dimensional data comparison + #[tool( + name = "render_radar", + description = "show a radar chart (spider chart) for multi-dimensional data comparison. The data must contain: +labels (Array of strings representing the dimensions/axes) and +datasets (Array of dataset objects with 'label' and 'data' properties)" + )] + pub async fn render_radar( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -683,23 +485,36 @@ impl AutoVisualiserRouter { let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { uri: "ui://radar/chart".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn render_treemap(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show pie or donut charts for categorical data visualization + #[tool( + name = "render_donut", + description = "show pie or donut charts for categorical data visualization. Supports single or multiple charts in a grid layout. Each chart should contain: +data (Array of values or objects with 'label' and 'value'), +type (Optional 'doughnut' or 'pie'), +title (Optional chart title), +labels (Optional array of labels if data is just numbers)" + )] + pub async fn render_donut( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -708,43 +523,56 @@ impl AutoVisualiserRouter { })?; // Load all resources at compile time using include_str! - const TEMPLATE: &str = include_str!("templates/treemap_template.html"); - const D3_MIN: &str = include_str!("templates/assets/d3.min.js"); + const TEMPLATE: &str = include_str!("templates/donut_template.html"); + const CHART_MIN: &str = include_str!("templates/assets/chart.min.js"); // Replace all placeholders with actual content let html_content = TEMPLATE - .replace("{{D3_MIN}}", D3_MIN) - .replace("{{TREEMAP_DATA}}", &data_json); + .replace("{{CHART_MIN}}", CHART_MIN) + .replace("{{CHARTS_DATA}}", &data_json); - // Save to /tmp/treemap.html for debugging - let debug_path = std::path::Path::new("/tmp/treemap.html"); + // Save to /tmp/donut.html for debugging + let debug_path = std::path::Path::new("/tmp/donut.html"); if let Err(e) = std::fs::write(debug_path, &html_content) { - tracing::warn!("Failed to write debug HTML to /tmp/treemap.html: {}", e); + tracing::warn!("Failed to write debug HTML to /tmp/donut.html: {}", e); } else { - tracing::info!("Debug HTML saved to /tmp/treemap.html"); + tracing::info!("Debug HTML saved to /tmp/donut.html"); } // Use BlobResourceContents with base64 encoding to avoid JSON string escaping issues let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { - uri: "ui://treemap/visualization".to_string(), + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { + uri: "ui://donut/chart".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn render_chord(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show a treemap visualization for hierarchical data with proportional area representation as boxes + #[tool( + name = "render_treemap", + description = "show a treemap visualization for hierarchical data with proportional area representation as boxes. The data should be a hierarchical structure with: +name (Name of the node, required), +value (Numeric value for leaf nodes, optional for parent nodes), +children (Array of child nodes, optional), +category (Category for coloring, optional)" + )] + pub async fn render_treemap( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -753,43 +581,54 @@ impl AutoVisualiserRouter { })?; // Load all resources at compile time using include_str! - const TEMPLATE: &str = include_str!("templates/chord_template.html"); + const TEMPLATE: &str = include_str!("templates/treemap_template.html"); const D3_MIN: &str = include_str!("templates/assets/d3.min.js"); // Replace all placeholders with actual content let html_content = TEMPLATE .replace("{{D3_MIN}}", D3_MIN) - .replace("{{CHORD_DATA}}", &data_json); + .replace("{{TREEMAP_DATA}}", &data_json); - // Save to /tmp/chord.html for debugging - let debug_path = std::path::Path::new("/tmp/chord.html"); + // Save to /tmp/treemap.html for debugging + let debug_path = std::path::Path::new("/tmp/treemap.html"); if let Err(e) = std::fs::write(debug_path, &html_content) { - tracing::warn!("Failed to write debug HTML to /tmp/chord.html: {}", e); + tracing::warn!("Failed to write debug HTML to /tmp/treemap.html: {}", e); } else { - tracing::info!("Debug HTML saved to /tmp/chord.html"); + tracing::info!("Debug HTML saved to /tmp/treemap.html"); } // Use BlobResourceContents with base64 encoding to avoid JSON string escaping issues let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { - uri: "ui://chord/diagram".to_string(), + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { + uri: "ui://treemap/visualization".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn render_donut(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, true)?; // true because donut accepts arrays + /// Show a chord diagram visualization for showing relationships and flows between entities + #[tool( + name = "render_chord", + description = "Show a chord diagram visualization for showing relationships and flows between entities. The data must contain: +labels (Array of strings representing the entities) and +matrix (2D array of numbers representing flows, matrix[i][j] = flow from i to j)" + )] + pub async fn render_chord( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -798,53 +637,67 @@ impl AutoVisualiserRouter { })?; // Load all resources at compile time using include_str! - const TEMPLATE: &str = include_str!("templates/donut_template.html"); - const CHART_MIN: &str = include_str!("templates/assets/chart.min.js"); + const TEMPLATE: &str = include_str!("templates/chord_template.html"); + const D3_MIN: &str = include_str!("templates/assets/d3.min.js"); // Replace all placeholders with actual content let html_content = TEMPLATE - .replace("{{CHART_MIN}}", CHART_MIN) - .replace("{{CHARTS_DATA}}", &data_json); + .replace("{{D3_MIN}}", D3_MIN) + .replace("{{CHORD_DATA}}", &data_json); - // Save to /tmp/donut.html for debugging - let debug_path = std::path::Path::new("/tmp/donut.html"); + // Save to /tmp/chord.html for debugging + let debug_path = std::path::Path::new("/tmp/chord.html"); if let Err(e) = std::fs::write(debug_path, &html_content) { - tracing::warn!("Failed to write debug HTML to /tmp/donut.html: {}", e); + tracing::warn!("Failed to write debug HTML to /tmp/chord.html: {}", e); } else { - tracing::info!("Debug HTML saved to /tmp/donut.html"); + tracing::info!("Debug HTML saved to /tmp/chord.html"); } // Use BlobResourceContents with base64 encoding to avoid JSON string escaping issues let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { - uri: "ui://donut/chart".to_string(), + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { + uri: "ui://chord/diagram".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn render_map(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show an interactive map visualization with location markers using Leaflet + #[tool( + name = "render_map", + description = "show an interactive map visualization with location markers using Leaflet. The data must contain: +markers (Array of objects with 'lat', 'lng', and optional properties), +title (Optional title for the map), +subtitle (Optional subtitle), +center (Optional center point {lat, lng}), +zoom (Optional initial zoom level), +clustering (Optional boolean to enable/disable clustering), +autoFit (Optional boolean to auto-fit map to markers)" + )] + pub async fn render_map( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Extract title and subtitle from data if provided - let title = data - .get("title") - .and_then(|v| v.as_str()) - .unwrap_or("Interactive Map"); - let subtitle = data - .get("subtitle") - .and_then(|v| v.as_str()) + let title = params.data.title.as_deref().unwrap_or("Interactive Map"); + let subtitle = params + .data + .subtitle + .as_deref() .unwrap_or("Geographic data visualization"); // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -880,23 +733,34 @@ impl AutoVisualiserRouter { let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { uri: "ui://map/visualization".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } - async fn show_chart(&self, params: Value) -> Result, ErrorData> { - let data = validate_data_param(¶ms, false)?; + /// Show interactive line, scatter, or bar charts + #[tool( + name = "show_chart", + description = "show interactive line, scatter, or bar charts. +Required: type ('line', 'scatter', or 'bar'), datasets array. +Optional: labels, title, subtitle, xAxisLabel, yAxisLabel, options" + )] + pub async fn show_chart( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Convert the data to JSON string - let data_json = serde_json::to_string(&data).map_err(|e| { + let data_json = serde_json::to_string(¶ms.data).map_err(|e| { ErrorData::new( ErrorCode::INVALID_PARAMS, format!("Invalid JSON data: {}", e), @@ -925,100 +789,27 @@ impl AutoVisualiserRouter { let html_bytes = html_content.as_bytes(); let base64_encoded = STANDARD.encode(html_bytes); - let resource_contents = ResourceContents::BlobResourceContents { + let resource_contents = rmcp::model::ResourceContents::BlobResourceContents { uri: "ui://chart/interactive".to_string(), mime_type: Some("text/html".to_string()), blob: base64_encoded, meta: None, }; - Ok(vec![ - Content::resource(resource_contents).with_audience(vec![Role::User]) - ]) + Ok(CallToolResult::success(vec![Content::resource( + resource_contents, + ) + .with_audience(vec![Role::User])])) } } -impl Router for AutoVisualiserRouter { - fn name(&self) -> String { - "AutoVisualiserExtension".to_string() - } - - fn instructions(&self) -> String { - self.instructions.clone() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new() - .with_tools(false) - .with_resources(false, false) - .build() - } - - fn list_tools(&self) -> Vec { - self.tools.clone() - } - - fn call_tool( - &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - Box::pin(async move { - match tool_name.as_str() { - "render_sankey" => this.render_sankey(arguments).await, - "render_radar" => this.render_radar(arguments).await, - "render_donut" => this.render_donut(arguments).await, - "render_treemap" => this.render_treemap(arguments).await, - "render_chord" => this.render_chord(arguments).await, - "render_map" => this.render_map(arguments).await, - "show_chart" => this.show_chart(arguments).await, - _ => Err(ErrorData::new( - ErrorCode::INVALID_REQUEST, - format!("Tool {} not found", tool_name), - None, - )), - } - }) - } - - fn list_resources(&self) -> Vec { - let active_resources = self.active_resources.lock().unwrap(); - let resources = active_resources.values().cloned().collect(); - tracing::info!("Listing resources: {:?}", resources); - resources - } - - fn read_resource( - &self, - uri: &str, - ) -> Pin> + Send + 'static>> { - let uri = uri.to_string(); - Box::pin(async move { - Err(ResourceError::NotFound(format!( - "Resource not found: {}", - uri - ))) - }) - } - - fn list_prompts(&self) -> Vec { - vec![] - } - - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) - }) +impl Clone for AutoVisualiserServer { + fn clone(&self) -> Self { + Self { + tool_router: Self::tool_router(), + instructions: self.instructions.clone(), + cache_dir: self.cache_dir.clone(), + } } } @@ -1031,11 +822,9 @@ mod tests { #[test] fn test_validate_data_param_rejects_string() { // Test that a string value for data is rejected - let params = json!({ - "data": "{\"labels\": [\"A\", \"B\"], \"matrix\": [[0, 1], [1, 0]]}" - }); + let data = json!("{\"labels\": [\"A\", \"B\"], \"matrix\": [[0, 1], [1, 0]]}"); - let result = validate_data_param(¶ms, false); + let result = AutoVisualiserServer::validate_data_param(&data, false); assert!(result.is_err()); let err = result.unwrap_err(); @@ -1049,32 +838,28 @@ mod tests { #[test] fn test_validate_data_param_accepts_object() { // Test that a proper object is accepted - let params = json!({ - "data": { - "labels": ["A", "B"], - "matrix": [[0, 1], [1, 0]] - } + let data = json!({ + "labels": ["A", "B"], + "matrix": [[0, 1], [1, 0]] }); - let result = validate_data_param(¶ms, false); + let result = AutoVisualiserServer::validate_data_param(&data, false); assert!(result.is_ok()); - let data = result.unwrap(); - assert!(data.is_object()); - assert_eq!(data["labels"][0], "A"); + let validated_data = result.unwrap(); + assert!(validated_data.is_object()); + assert_eq!(validated_data["labels"][0], "A"); } #[test] fn test_validate_data_param_rejects_array_when_not_allowed() { // Test that an array is rejected when allow_array is false - let params = json!({ - "data": [ - {"label": "A", "value": 10}, - {"label": "B", "value": 20} - ] - }); + let data = json!([ + {"label": "A", "value": 10}, + {"label": "B", "value": 20} + ]); - let result = validate_data_param(¶ms, false); + let result = AutoVisualiserServer::validate_data_param(&data, false); assert!(result.is_err()); let err = result.unwrap_err(); @@ -1085,75 +870,49 @@ mod tests { #[test] fn test_validate_data_param_accepts_array_when_allowed() { // Test that an array is accepted when allow_array is true - let params = json!({ - "data": [ - {"label": "A", "value": 10}, - {"label": "B", "value": 20} - ] - }); + let data = json!([ + {"label": "A", "value": 10}, + {"label": "B", "value": 20} + ]); - let result = validate_data_param(¶ms, true); + let result = AutoVisualiserServer::validate_data_param(&data, true); assert!(result.is_ok()); - let data = result.unwrap(); - assert!(data.is_array()); - assert_eq!(data[0]["label"], "A"); - } - - #[test] - fn test_validate_data_param_missing_data() { - // Test that missing data parameter is rejected - let params = json!({ - "other": "value" - }); - - let result = validate_data_param(¶ms, false); - assert!(result.is_err()); - - let err = result.unwrap_err(); - assert_eq!(err.code, ErrorCode::INVALID_PARAMS); - assert!(err.message.contains("Missing 'data' parameter")); + let validated_data = result.unwrap(); + assert!(validated_data.is_array()); + assert_eq!(validated_data[0]["label"], "A"); } #[test] fn test_validate_data_param_rejects_primitive_values() { // Test that primitive values (number, boolean) are rejected - let params_number = json!({ - "data": 42 - }); - - let result = validate_data_param(¶ms_number, false); + let data_number = json!(42); + let result = AutoVisualiserServer::validate_data_param(&data_number, false); assert!(result.is_err()); - let params_bool = json!({ - "data": true - }); - - let result = validate_data_param(¶ms_bool, false); + let data_bool = json!(true); + let result = AutoVisualiserServer::validate_data_param(&data_bool, false); assert!(result.is_err()); - let params_null = json!({ - "data": null - }); - - let result = validate_data_param(¶ms_null, false); + let data_null = json!(null); + let result = AutoVisualiserServer::validate_data_param(&data_null, false); assert!(result.is_err()); } #[test] fn test_validate_data_param_with_json_containing_comments_as_string() { // Test that JSON with comments passed as a string is rejected - let params = json!({ - "data": r#"{ - "labels": ["A", "B"], - "matrix": [ - [0, 1], // This is a comment - [1, 0] /* Another comment */ - ] - }"# - }); + let data = json!( + r#"{ + "labels": ["A", "B"], + "matrix": [ + [0, 1], // This is a comment + [1, 0] /* Another comment */ + ] + }"# + ); - let result = validate_data_param(¶ms, false); + let result = AutoVisualiserServer::validate_data_param(&data, false); assert!(result.is_err()); let err = result.unwrap_err(); @@ -1164,17 +923,32 @@ mod tests { #[tokio::test] async fn test_render_sankey() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "nodes": [{"name": "A"}, {"name": "B"}], - "links": [{"source": "A", "target": "B", "value": 10}] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderSankeyParams { + data: SankeyData { + nodes: vec![ + SankeyNode { + name: "A".to_string(), + category: None, + }, + SankeyNode { + name: "B".to_string(), + category: None, + }, + ], + links: vec![SankeyLink { + source: "A".to_string(), + target: "B".to_string(), + value: 10.0, + }], + }, }); - let result = router.render_sankey(params).await; + let result = server.render_sankey(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1182,9 +956,8 @@ mod tests { assert_eq!(content[0].audience().unwrap(), &vec![Role::User]); // Check it's a resource with HTML content - // Content is Annotated, access underlying RawContent via * if let RawContent::Resource(resource) = &*content[0] { - if let ResourceContents::BlobResourceContents { uri, mime_type, .. } = + if let rmcp::model::ResourceContents::BlobResourceContents { uri, mime_type, .. } = &resource.resource { assert_eq!(uri, "ui://sankey/diagram"); @@ -1199,19 +972,26 @@ mod tests { #[tokio::test] async fn test_render_radar() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "categories": ["Speed", "Power", "Agility"], - "series": [ - {"label": "Player 1", "data": [80, 90, 85]} - ] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderRadarParams { + data: RadarData { + labels: vec![ + "Speed".to_string(), + "Power".to_string(), + "Agility".to_string(), + ], + datasets: vec![RadarDataset { + label: "Player 1".to_string(), + data: vec![80.0, 90.0, 85.0], + }], + }, }); - let result = router.render_radar(params).await; + let result = server.render_radar(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1219,9 +999,8 @@ mod tests { assert_eq!(content[0].audience().unwrap(), &vec![Role::User]); // Check it's a resource with HTML content - // Content is Annotated, access underlying RawContent via * if let RawContent::Resource(resource) = &*content[0] { - if let ResourceContents::BlobResourceContents { + if let rmcp::model::ResourceContents::BlobResourceContents { uri, mime_type, blob, @@ -1241,17 +1020,34 @@ mod tests { #[tokio::test] async fn test_render_donut() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "labels": ["A", "B", "C"], - "values": [30, 40, 30] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderDonutParams { + data: DonutData::Single(DonutChart { + title: None, + chart_type: None, + labels: Some(vec!["A".to_string(), "B".to_string(), "C".to_string()]), + data: vec![ + DonutDataItem::LabeledValue { + label: "A".to_string(), + value: 30.0, + }, + DonutDataItem::LabeledValue { + label: "B".to_string(), + value: 40.0, + }, + DonutDataItem::LabeledValue { + label: "C".to_string(), + value: 30.0, + }, + ], + }), }); - let result = router.render_donut(params).await; + let result = server.render_donut(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1261,20 +1057,34 @@ mod tests { #[tokio::test] async fn test_render_treemap() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "name": "root", - "children": [ - {"name": "A", "value": 100}, - {"name": "B", "value": 200} - ] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderTreemapParams { + data: TreemapNode { + name: "root".to_string(), + value: None, + category: None, + children: Some(vec![ + TreemapNode { + name: "A".to_string(), + value: Some(100.0), + category: None, + children: None, + }, + TreemapNode { + name: "B".to_string(), + value: Some(200.0), + category: None, + children: None, + }, + ]), + }, }); - let result = router.render_treemap(params).await; + let result = server.render_treemap(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1284,17 +1094,23 @@ mod tests { #[tokio::test] async fn test_render_chord() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "labels": ["A", "B", "C"], - "matrix": [[0, 10, 5], [10, 0, 15], [5, 15, 0]] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderChordParams { + data: ChordData { + labels: vec!["A".to_string(), "B".to_string(), "C".to_string()], + matrix: vec![ + vec![0.0, 10.0, 5.0], + vec![10.0, 0.0, 15.0], + vec![5.0, 15.0, 0.0], + ], + }, }); - let result = router.render_chord(params).await; + let result = server.render_chord(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1304,22 +1120,34 @@ mod tests { #[tokio::test] async fn test_render_map() { - let router = AutoVisualiserRouter::new(); - let params = json!({ - "data": { - "features": [ - { - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [0, 0]}, - "properties": {"name": "Origin"} - } - ] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(RenderMapParams { + data: MapData { + markers: vec![MapMarker { + lat: 0.0, + lng: 0.0, + name: Some("Origin".to_string()), + value: None, + description: None, + popup: None, + color: None, + label: None, + use_default_icon: None, + }], + title: None, + subtitle: None, + center: None, + zoom: None, + clustering: None, + auto_fit: None, + }, }); - let result = router.render_map(params).await; + let result = server.render_map(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User @@ -1329,28 +1157,31 @@ mod tests { #[tokio::test] async fn test_show_chart() { - let router = AutoVisualiserRouter::new(); - // show_chart expects data to be an object, not an array - let params = json!({ - "data": { - "datasets": [ - { - "label": "Test Data", - "data": [ - {"x": 1, "y": 2}, - {"x": 2, "y": 4} - ] - } - ] - } + let server = AutoVisualiserServer::new(); + let params = Parameters(ShowChartParams { + data: ChartData { + chart_type: ChartDataType::Line, + datasets: vec![ChartDataset { + label: "Test Data".to_string(), + data: vec![ + ChartDataPoint::Coordinate { x: 1.0, y: 2.0 }, + ChartDataPoint::Coordinate { x: 2.0, y: 4.0 }, + ], + }], + labels: None, + title: None, + subtitle: None, + x_axis_label: None, + y_axis_label: None, + options: None, + }, }); - let result = router.show_chart(params).await; - if let Err(e) = &result { - eprintln!("Error in test_show_chart: {:?}", e); - } + let result = server.show_chart(params).await; assert!(result.is_ok()); - let content = result.unwrap(); + let call_result = result.unwrap(); + + let content = call_result.content; assert_eq!(content.len(), 1); // Check the audience is set to User diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 8edd4cd59ee8..b8bffb075d2a 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -1,426 +1,132 @@ -use base64::Engine; use etcetera::{choose_app_strategy, AppStrategy}; use indoc::{formatdoc, indoc}; use reqwest::{Client, Url}; -use serde_json::Value; -use std::borrow::Cow; -use std::{ - collections::HashMap, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex, +use rmcp::{ + handler::server::router::tool::ToolRouter, + model::{ + AnnotateAble, CallToolResult, Content, ErrorCode, ErrorData, Implementation, Role, + ServerCapabilities, ServerInfo, + }, + schemars::JsonSchema, + tool, tool_handler, tool_router, ServerHandler, }; -use tokio::{process::Command, sync::mpsc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, sync::Mutex}; +use tokio::process::Command; +use rmcp::handler::server::wrapper::Parameters; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; - -use mcp_core::{ - handler::{require_str_parameter, require_u64_parameter, PromptError, ResourceError}, - protocol::ServerCapabilities, -}; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::{ - AnnotateAble, Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, RawResource, Resource, - Tool, ToolAnnotations, -}; -use rmcp::object; - mod docx_tool; mod pdf_tool; -mod xlsx_tool; - mod platform; +mod xlsx_tool; use platform::{create_system_automation, SystemAutomation}; -/// An extension designed for non-developers to help them with common tasks like -/// web scraping, data processing, and automation. -#[derive(Clone)] -pub struct ComputerControllerRouter { - tools: Vec, - cache_dir: PathBuf, - active_resources: Arc>>, - http_client: Client, - instructions: String, - system_automation: Arc>, +/// Parameter struct for web_scrape tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct WebScrapeParams { + /// The URL to fetch content from + pub url: String, + /// How to interpret and save the content + #[serde(default = "default_save_as")] + pub save_as: String, } -impl Default for ComputerControllerRouter { - fn default() -> Self { - Self::new() - } +fn default_save_as() -> String { + "text".to_string() } -impl ComputerControllerRouter { - pub fn new() -> Self { - let web_scrape_tool = Tool::new( - "web_scrape", - indoc! {r#" - Fetch and save content from a web page. The content can be saved as: - - text (for HTML pages) - - json (for API responses) - - binary (for images and other files) - - The content is cached locally and can be accessed later using the cache_path - returned in the response. - "#}, - object!({ - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch content from" - }, - "save_as": { - "type": "string", - "enum": ["text", "json", "binary"], - "default": "text", - "description": "How to interpret and save the content" - } - } - }), - ) - .annotate(ToolAnnotations { - title: Some("Web Scrape".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(true), - }); - - let computer_control_desc = match std::env::consts::OS { - "windows" => indoc! {r#" - Control the computer using Windows system automation. - - Features available: - - PowerShell automation for system control - - UI automation through PowerShell - - File and system management - - Windows-specific features and settings - - Can be combined with screenshot tool for visual task assistance. - "#}, - "macos" => indoc! {r#" - Control the computer using AppleScript (macOS only). Automate applications and system features. - - Key capabilities: - - Control Applications: Launch, quit, manage apps (Mail, Safari, iTunes, etc) - - Interact with app-specific feature: (e.g, edit documents, process photos) - - Perform tasks in third-party apps that support AppleScript - - UI Automation: Simulate user interactions like, clicking buttons, select menus, type text, filling out forms - - System Control: Manage settings (volume, brightness, wifi), shutdown/restart, monitor events - - Web & Email: Open URLs, web automation, send/organize emails, handle attachments - - Media: Manage music libraries, photo collections, playlists - - File Operations: Organize files/folders - - Integration: Calendar, reminders, messages - - Data: Interact with spreadsheets and documents - - Can be combined with screenshot tool for visual task assistance. - "#}, - _ => indoc! {r#" - Control the computer using Linux system automation. +/// Parameter struct for automation_script tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AutomationScriptParams { + /// The scripting language to use + pub language: String, + /// The script content + pub script: String, + /// Whether to save the script output to a file + #[serde(default)] + pub save_output: bool, +} - Features available: - - Shell scripting for system control - - X11/Wayland window management - - D-Bus for system services - - File and system management - - Desktop environment control (GNOME, KDE, etc.) - - Process management and monitoring - - System settings and configurations +/// Parameter struct for computer_control tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct ComputerControlParams { + /// The automation script content (PowerShell for Windows, AppleScript for macOS) + pub script: String, + /// Whether to save the script output to a file + #[serde(default)] + pub save_output: bool, +} - Can be combined with screenshot tool for visual task assistance. - "#}, - }; +/// Parameter struct for cache tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct CacheParams { + /// The command to perform + pub command: String, + /// Path to the cached file for view/delete commands + pub path: Option, +} - let computer_control_tool = Tool::new( - "computer_control", - computer_control_desc.to_string(), - object!({ - "type": "object", - "required": ["script"], - "properties": { - "script": { - "type": "string", - "description": "The automation script content (PowerShell for Windows, AppleScript for macOS)" - }, - "save_output": { - "type": "boolean", - "default": false, - "description": "Whether to save the script output to a file" - } - } - }), - ); +/// Parameter struct for pdf_tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct PdfToolParams { + /// Path to the PDF file + pub path: String, + /// Operation to perform on the PDF + pub operation: String, +} - let quick_script_desc = match std::env::consts::OS { - "windows" => indoc! {r#" - Create and run small PowerShell or Batch scripts for automation tasks. - PowerShell is recommended for most tasks. - - The script is saved to a temporary file and executed. - Some examples: - - Sort unique lines: Get-Content file.txt | Sort-Object -Unique - - Extract CSV column: Import-Csv file.csv | Select-Object -ExpandProperty Column2 - - Find text: Select-String -Pattern "pattern" -Path file.txt - "#}, - _ => indoc! {r#" - Create and run small scripts for automation tasks. - Supports Shell and Ruby (on macOS). - - The script is saved to a temporary file and executed. - Consider using shell script (bash) for most simple tasks first. - Ruby is useful for text processing or when you need more sophisticated scripting capabilities. - Some examples of shell: - - create a sorted list of unique lines: sort file.txt | uniq - - extract 2nd column in csv: awk -F "," '{ print $2}' - - pattern matching: grep pattern file.txt - "#}, - }; +/// Parameter struct for docx_tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct DocxToolParams { + /// Path to the DOCX file + pub path: String, + /// Operation to perform on the DOCX + pub operation: String, + /// Content to write (required for update_doc operation) + pub content: Option, + /// Additional parameters for update_doc operation + pub params: Option, +} - let quick_script_tool = Tool::new( - "automation_script", - quick_script_desc.to_string(), - object!({ - "type": "object", - "required": ["language", "script"], - "properties": { - "language": { - "type": "string", - "enum": ["shell", "ruby", "powershell", "batch"], - "description": "The scripting language to use" - }, - "script": { - "type": "string", - "description": "The script content" - }, - "save_output": { - "type": "boolean", - "default": false, - "description": "Whether to save the script output to a file" - } - } - }), - ); - - let cache_tool = Tool::new( - "cache", - indoc! {r#" - Manage cached files and data: - - list: List all cached files - - view: View content of a cached file - - delete: Delete a cached file - - clear: Clear all cached files - "#}, - object!({ - "type": "object", - "required": ["command"], - "properties": { - "command": { - "type": "string", - "enum": ["list", "view", "delete", "clear"], - "description": "The command to perform" - }, - "path": { - "type": "string", - "description": "Path to the cached file for view/delete commands" - } - } - }), - ); - - let pdf_tool = Tool::new( - "pdf_tool", - indoc! {r#" - Process PDF files to extract text and images. - Supports operations: - - extract_text: Extract all text content from the PDF - - extract_images: Extract and save embedded images to PNG files - - Use this when there is a .pdf file or files that need to be processed. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the PDF file" - }, - "operation": { - "type": "string", - "enum": ["extract_text", "extract_images"], - "description": "Operation to perform on the PDF" - } - } - }), - ) - .annotate(ToolAnnotations { - title: Some("PDF process".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(true), - open_world_hint: Some(false), - }); +/// Parameter struct for xlsx_tool +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct XlsxToolParams { + /// Path to the XLSX file + pub path: String, + /// Operation to perform on the XLSX file + pub operation: String, + /// Worksheet name (if not provided, uses first worksheet) + pub worksheet: Option, + /// Cell range in A1 notation (e.g., 'A1:C10') for get_range operation + pub range: Option, + /// Text to search for in find_text operation + pub search_text: Option, + /// Whether search should be case-sensitive + #[serde(default)] + pub case_sensitive: bool, + /// Row number for update_cell and get_cell operations + pub row: Option, + /// Column number for update_cell and get_cell operations + pub col: Option, + /// New value for update_cell operation + pub value: Option, +} - let docx_tool = Tool::new( - "docx_tool", - indoc! {r#" - Process DOCX files to extract text and create/update documents. - Supports operations: - - extract_text: Extract all text content and structure (headings, TOC) from the DOCX - - update_doc: Create a new DOCX or update existing one with provided content - Modes: - - append: Add content to end of document (default) - - replace: Replace specific text with new content - - structured: Add content with specific heading level and styling - - add_image: Add an image to the document (with optional caption) - - Use this when there is a .docx file that needs to be processed or created. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the DOCX file" - }, - "operation": { - "type": "string", - "enum": ["extract_text", "update_doc"], - "description": "Operation to perform on the DOCX" - }, - "content": { - "type": "string", - "description": "Content to write (required for update_doc operation)" - }, - "params": { - "type": "object", - "description": "Additional parameters for update_doc operation", - "properties": { - "mode": { - "type": "string", - "enum": ["append", "replace", "structured", "add_image"], - "description": "Update mode (default: append)" - }, - "old_text": { - "type": "string", - "description": "Text to replace (required for replace mode)" - }, - "level": { - "type": "string", - "description": "Heading level for structured mode (e.g., 'Heading1', 'Heading2')" - }, - "image_path": { - "type": "string", - "description": "Path to the image file (required for add_image mode)" - }, - "width": { - "type": "integer", - "description": "Image width in pixels (optional)" - }, - "height": { - "type": "integer", - "description": "Image height in pixels (optional)" - }, - "style": { - "type": "object", - "description": "Styling options for the text", - "properties": { - "bold": { - "type": "boolean", - "description": "Make text bold" - }, - "italic": { - "type": "boolean", - "description": "Make text italic" - }, - "underline": { - "type": "boolean", - "description": "Make text underlined" - }, - "size": { - "type": "integer", - "description": "Font size in points" - }, - "color": { - "type": "string", - "description": "Text color in hex format (e.g., 'FF0000' for red)" - }, - "alignment": { - "type": "string", - "enum": ["left", "center", "right", "justified"], - "description": "Text alignment" - } - } - } - } - } - } - }), - ); - - let xlsx_tool = Tool::new( - "xlsx_tool", - indoc! {r#" - Process Excel (XLSX) files to read and manipulate spreadsheet data. - Supports operations: - - list_worksheets: List all worksheets in the workbook (returns name, index, column_count, row_count) - - get_columns: Get column names from a worksheet (returns values from the first row) - - get_range: Get values and formulas from a cell range (e.g., "A1:C10") (returns a 2D array organized as [row][column]) - - find_text: Search for text in a worksheet (returns a list of (row, column) coordinates) - - update_cell: Update a single cell's value (returns confirmation message) - - get_cell: Get value and formula from a specific cell (returns both value and formula if present) - - save: Save changes back to the file (returns confirmation message) - - Use this when working with Excel spreadsheets to analyze or modify data. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the XLSX file" - }, - "operation": { - "type": "string", - "enum": ["list_worksheets", "get_columns", "get_range", "find_text", "update_cell", "get_cell", "save"], - "description": "Operation to perform on the XLSX file" - }, - "worksheet": { - "type": "string", - "description": "Worksheet name (if not provided, uses first worksheet)" - }, - "range": { - "type": "string", - "description": "Cell range in A1 notation (e.g., 'A1:C10') for get_range operation" - }, - "search_text": { - "type": "string", - "description": "Text to search for in find_text operation" - }, - "case_sensitive": { - "type": "boolean", - "default": false, - "description": "Whether search should be case-sensitive" - }, - "row": { - "type": "integer", - "description": "Row number for update_cell and get_cell operations" - }, - "col": { - "type": "integer", - "description": "Column number for update_cell and get_cell operations" - }, - "value": { - "type": "string", - "description": "New value for update_cell operation" - } - } - }), - ); +/// ComputerController MCP Server using official RMCP SDK +pub struct ComputerControllerServer { + tool_router: ToolRouter, + cache_dir: PathBuf, + active_resources: Arc>>, + http_client: Client, + instructions: String, + system_automation: Arc>, +} +impl ComputerControllerServer { + pub fn new() -> Self { // choose_app_strategy().cache_dir() // - macOS/Linux: ~/.cache/goose/computer_controller/ // - Windows: ~\AppData\Local\Block\goose\cache\computer_controller\ @@ -540,15 +246,7 @@ impl ComputerControllerRouter { }; Self { - tools: vec![ - web_scrape_tool, - quick_script_tool, - computer_control_tool, - cache_tool, - pdf_tool, - docx_tool, - xlsx_tool, - ], + tool_router: Self::tool_router(), cache_dir, active_resources: Arc::new(Mutex::new(HashMap::new())), http_client: Client::builder().user_agent("Goose/1.0").build().unwrap(), @@ -570,27 +268,20 @@ impl ComputerControllerRouter { content: &[u8], prefix: &str, extension: &str, - ) -> Result { + ) -> Result { let cache_path = self.get_cache_path(prefix, extension); - fs::write(&cache_path, content).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write to cache: {}", e)), - data: None, - })?; + fs::write(&cache_path, content).map_err(|e| format!("Failed to write to cache: {}", e))?; Ok(cache_path) } // Helper function to register a file as a resource - fn register_as_resource(&self, cache_path: &PathBuf, mime_type: &str) -> Result<(), ErrorData> { + fn register_as_resource(&self, cache_path: &PathBuf, mime_type: &str) -> Result<(), String> { let uri = Url::from_file_path(cache_path) - .map_err(|_| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from("Invalid cache path"), - data: None, - })? + .map_err(|_| "Invalid cache path".to_string())? .to_string(); - let mut resource = RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned()); + let mut resource = + rmcp::model::RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned()); resource.mime_type = Some(if mime_type == "blob" { "blob".to_string() } else { @@ -602,160 +293,226 @@ impl ComputerControllerRouter { .insert(uri, resource.no_annotation()); Ok(()) } +} - async fn web_scrape(&self, params: Value) -> Result, ErrorData> { - let url = params - .get("url") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'url' parameter"), - data: None, - })?; +impl Default for ComputerControllerServer { + fn default() -> Self { + Self::new() + } +} + +impl Clone for ComputerControllerServer { + fn clone(&self) -> Self { + Self { + tool_router: Self::tool_router(), + cache_dir: self.cache_dir.clone(), + active_resources: self.active_resources.clone(), + http_client: self.http_client.clone(), + instructions: self.instructions.clone(), + system_automation: self.system_automation.clone(), + } + } +} - let save_as = params - .get("save_as") - .and_then(|v| v.as_str()) - .unwrap_or("text"); +#[tool_handler(router = self.tool_router)] +impl ServerHandler for ComputerControllerServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "goose-computercontroller".to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + capabilities: ServerCapabilities::builder().enable_tools().build(), + instructions: Some(self.instructions.clone()), + ..Default::default() + } + } +} +#[tool_router(router = tool_router)] +impl ComputerControllerServer { + /// Fetch and save content from a web page. The content can be saved as: + /// - text (for HTML pages) + /// - json (for API responses) + /// - binary (for images and other files) + /// + /// The content is cached locally and can be accessed later using the cache_path + /// returned in the response. + #[tool( + name = "web_scrape", + description = "Fetch and save content from a web page. The content can be saved as: +- text (for HTML pages) +- json (for API responses) +- binary (for images and other files) + +The content is cached locally and can be accessed later using the cache_path +returned in the response." + )] + pub async fn web_scrape( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Fetch the content let response = self .http_client - .get(url) + .get(¶ms.url) .send() .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to fetch URL: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to fetch URL: {}", e), + None, + ) })?; let status = response.status(); if !status.is_success() { - return Err(ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("HTTP request failed with status: {}", status)), - data: None, - }); + return Err(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("HTTP request failed with status: {}", status), + None, + )); } // Process based on save_as parameter - let (content, extension) = match save_as { + let (content, extension) = match params.save_as.as_str() { "text" => { - let text = response.text().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get text: {}", e)), - data: None, + let text = response.text().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get text: {}", e), + None, + ) })?; (text.into_bytes(), "txt") } "json" => { - let text = response.text().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get text: {}", e)), - data: None, + let text = response.text().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get text: {}", e), + None, + ) })?; // Verify it's valid JSON - serde_json::from_str::(&text).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Invalid JSON response: {}", e)), - data: None, + serde_json::from_str::(&text).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Invalid JSON response: {}", e), + None, + ) })?; (text.into_bytes(), "json") } "binary" => { - let bytes = response.bytes().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get bytes: {}", e)), - data: None, + let bytes = response.bytes().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get bytes: {}", e), + None, + ) })?; (bytes.to_vec(), "bin") } _ => { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "Invalid 'save_as' parameter: {}. Valid options are: 'text', 'json', 'binary'", - save_as - )), - data: None, - }); + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + format!( + "Invalid 'save_as' parameter: {}. Valid options are: 'text', 'json', 'binary'", + params.save_as + ), + None, + )); } }; // Save to cache - let cache_path = self.save_to_cache(&content, "web", extension).await?; + let cache_path = self + .save_to_cache(&content, "web", extension) + .await + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; // Register as a resource - self.register_as_resource(&cache_path, save_as)?; + self.register_as_resource(&cache_path, ¶ms.save_as) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; - Ok(vec![Content::text(format!( + Ok(CallToolResult::success(vec![Content::text(format!( "Content saved to: {}", cache_path.display() - ))]) + )) + .with_audience(vec![Role::Assistant])])) } - // Implement quick_script tool functionality - async fn quick_script(&self, params: Value) -> Result, ErrorData> { - let language = params - .get("language") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'language' parameter"), - data: None, - })?; - - let script = params - .get("script") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'script' parameter"), - data: None, - })?; - - let save_output = params - .get("save_output") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - + /// Create and run small scripts for automation tasks. + /// Supports Shell and Ruby (on macOS). + /// + /// The script is saved to a temporary file and executed. + /// Consider using shell script (bash) for most simple tasks first. + /// Ruby is useful for text processing or when you need more sophisticated scripting capabilities. + #[tool( + name = "automation_script", + description = "Create and run small scripts for automation tasks. +Supports Shell and Ruby (on macOS). + +The script is saved to a temporary file and executed. +Consider using shell script (bash) for most simple tasks first. +Ruby is useful for text processing or when you need more sophisticated scripting capabilities. +Some examples of shell: + - create a sorted list of unique lines: sort file.txt | uniq + - extract 2nd column in csv: awk -F \",\" '{ print $2}' + - pattern matching: grep pattern file.txt" + )] + pub async fn automation_script( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Create a temporary directory for the script - let script_dir = tempfile::tempdir().map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to create temporary directory: {}", e)), - data: None, + let script_dir = tempfile::tempdir().map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to create temporary directory: {}", e), + None, + ) })?; let (shell, shell_arg) = self.system_automation.get_shell_command(); - let command = match language { + let command = match params.language.as_str() { "shell" | "batch" => { let script_path = script_dir.path().join(format!( "script.{}", if cfg!(windows) { "bat" } else { "sh" } )); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, ¶ms.script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; // Set execute permissions on Unix systems #[cfg(unix)] { let mut perms = fs::metadata(&script_path) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get file metadata: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get file metadata: {}", e), + None, + ) })? .permissions(); perms.set_mode(0o755); // rwxr-xr-x - fs::set_permissions(&script_path, perms).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to set execute permissions: {}", e)), - data: None, + fs::set_permissions(&script_path, perms).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to set execute permissions: {}", e), + None, + ) })?; } @@ -763,35 +520,35 @@ impl ComputerControllerRouter { } "ruby" => { let script_path = script_dir.path().join("script.rb"); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, ¶ms.script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; format!("ruby {}", script_path.display()) } "powershell" => { let script_path = script_dir.path().join("script.ps1"); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, ¶ms.script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; script_path.display().to_string() } _ => { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!("Invalid 'language' parameter: {}. Valid options are: 'shell', 'batch', 'ruby', 'powershell'", language)), - data: None, - }); + return Err(ErrorData::new(ErrorCode::INVALID_PARAMS, format!("Invalid 'language' parameter: {}. Valid options are: 'shell', 'batch', 'ruby', 'powershell'", params.language), None)); } }; // Run the script - let output = match language { + let output = match params.language.as_str() { "powershell" => { // For PowerShell, we need to use -File instead of -Command Command::new("powershell") @@ -802,10 +559,12 @@ impl ComputerControllerRouter { .env("GOOSE_TERMINAL", "1") .output() .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to run script: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to run script: {}", e), + None, + ) })? } _ => Command::new(shell) @@ -814,10 +573,12 @@ impl ComputerControllerRouter { .env("GOOSE_TERMINAL", "1") .output() .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to run script: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to run script: {}", e), + None, + ) })?, }; @@ -834,571 +595,435 @@ impl ComputerControllerRouter { }; // Save output if requested - if save_output && !output_str.is_empty() { + if params.save_output && !output_str.is_empty() { let cache_path = self .save_to_cache(output_str.as_bytes(), "script_output", "txt") - .await?; + .await + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; result.push_str(&format!("\n\nOutput saved to: {}", cache_path.display())); // Register as a resource - self.register_as_resource(&cache_path, "text")?; + self.register_as_resource(&cache_path, "text") + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; } - Ok(vec![Content::text(result)]) + Ok(CallToolResult::success(vec![ + Content::text(result).with_audience(vec![Role::Assistant]) + ])) } - // Implement computer control functionality - async fn computer_control(&self, params: Value) -> Result, ErrorData> { - let script = params - .get("script") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'script' parameter"), - data: None, - })?; - - let save_output = params - .get("save_output") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - + /// Control the computer using system automation. + /// Features available vary by platform: + /// - Windows: PowerShell automation for system control + /// - macOS: AppleScript for application and system control + /// - Linux: Shell scripting for system control + /// + /// Can be combined with screenshot tool for visual task assistance. + #[tool( + name = "computer_control", + description = "Control the computer using AppleScript (macOS only). Automate applications and system features. + +Key capabilities: +- Control Applications: Launch, quit, manage apps (Mail, Safari, iTunes, etc) + - Interact with app-specific feature: (e.g, edit documents, process photos) + - Perform tasks in third-party apps that support AppleScript +- UI Automation: Simulate user interactions like, clicking buttons, select menus, type text, filling out forms +- System Control: Manage settings (volume, brightness, wifi), shutdown/restart, monitor events +- Web & Email: Open URLs, web automation, send/organize emails, handle attachments +- Media: Manage music libraries, photo collections, playlists +- File Operations: Organize files/folders +- Integration: Calendar, reminders, messages +- Data: Interact with spreadsheets and documents + +Can be combined with screenshot tool for visual task assistance." + )] + pub async fn computer_control( + &self, + params: Parameters, + ) -> Result { + let params = params.0; // Use platform-specific automation let output = self .system_automation - .execute_system_script(script) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to execute script: {}", e)), - data: None, + .execute_system_script(¶ms.script) + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to execute script: {}", e), + None, + ) })?; let mut result = format!("Script completed successfully.\n\nOutput:\n{}", output); // Save output if requested - if save_output && !output.is_empty() { + if params.save_output && !output.is_empty() { let cache_path = self .save_to_cache(output.as_bytes(), "automation_output", "txt") - .await?; + .await + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; result.push_str(&format!("\n\nOutput saved to: {}", cache_path.display())); // Register as a resource - self.register_as_resource(&cache_path, "text")?; + self.register_as_resource(&cache_path, "text") + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e, None))?; } - Ok(vec![Content::text(result)]) + Ok(CallToolResult::success(vec![ + Content::text(result).with_audience(vec![Role::Assistant]) + ])) } - async fn xlsx_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; - - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; - - match operation { - "list_worksheets" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheets = xlsx.list_worksheets().map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", worksheets))]) - } - "get_columns" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - }; - let columns = xlsx.get_column_names(worksheet).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", columns))]) - } - "get_range" => { - let range = params - .get("range") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'range' parameter"), - data: None, - })?; - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - }; - let range_data = xlsx.get_range(worksheet, range).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", range_data))]) - } - "find_text" => { - let search_text = params - .get("search_text") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'search_text' parameter"), - data: None, - })?; - - let case_sensitive = params - .get("case_sensitive") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - }; - let matches = xlsx - .find_in_worksheet(worksheet, search_text, case_sensitive) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!( - "Found matches at: {:#?}", - matches - ))]) - } - "update_cell" => { - let row = require_u64_parameter(¶ms, "row")?; - let col = require_u64_parameter(¶ms, "col")?; - let value = require_str_parameter(¶ms, "value")?; - - let worksheet_name = params - .get("worksheet") - .and_then(|v| v.as_str()) - .unwrap_or("Sheet1"); - - let mut xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - xlsx.update_cell(worksheet_name, row as u32, col as u32, value) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - xlsx.save(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!( - "Updated cell ({}, {}) to '{}' in worksheet '{}'", - row, col, value, worksheet_name - ))]) - } - "save" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - xlsx.save(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text("File saved successfully.")]) - } - "get_cell" => { - let row = params - .get("row") - .and_then(|v| v.as_u64()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'row' parameter"), - data: None, - })?; - - let col = params - .get("col") - .and_then(|v| v.as_u64()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'col' parameter"), - data: None, - })?; - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })? - }; - let cell_value = xlsx - .get_cell_value(worksheet, row as u32, col as u32) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", cell_value))]) - } - _ => Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!("Invalid operation: {}", operation)), - data: None, - }), - } - } - - // Implement cache tool functionality - async fn docx_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; - - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; - - crate::computercontroller::docx_tool::docx_tool( - path, - operation, - params.get("content").and_then(|v| v.as_str()), - params.get("params"), - ) - .await - } - - async fn pdf_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; - - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; - - crate::computercontroller::pdf_tool::pdf_tool(path, operation, &self.cache_dir).await - } - - async fn cache(&self, params: Value) -> Result, ErrorData> { - let command = params - .get("command") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'command' parameter"), - data: None, - })?; - - match command { + /// Manage cached files and data: + /// - list: List all cached files + /// - view: View content of a cached file + /// - delete: Delete a cached file + /// - clear: Clear all cached files + #[tool( + name = "cache", + description = "Manage cached files and data: list, view, delete, or clear files." + )] + pub async fn cache( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let result = match params.command.as_str() { "list" => { let mut files = Vec::new(); - for entry in fs::read_dir(&self.cache_dir).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read cache directory: {}", e)), - data: None, + for entry in fs::read_dir(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read cache directory: {}", e), + None, + ) })? { - let entry = entry.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read directory entry: {}", e)), - data: None, + let entry = entry.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read directory entry: {}", e), + None, + ) })?; files.push(format!("{}", entry.path().display())); } files.sort(); - Ok(vec![Content::text(format!( - "Cached files:\n{}", - files.join("\n") - ))]) + format!("Cached files:\n{}", files.join("\n")) } "view" => { - let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| - ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter for view"), - data: None, - })?; + let path = params.path.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'path' parameter for view".to_string(), + None, + ) + })?; - let content = fs::read_to_string(path).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read file: {}", e)), - data: None, - })?; + let content = fs::read_to_string(&path).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read file: {}", e), + None, + ) + })?; - Ok(vec![Content::text(format!( - "Content of {}:\n\n{}", - path, content - ))]) + format!("Content of {}:\n\n{}", path, content) } "delete" => { - let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| - ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter for delete"), - data: None, - })?; + let path = params.path.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'path' parameter for delete".to_string(), + None, + ) + })?; - fs::remove_file(path).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to delete file: {}", e)), - data: None, - })?; + fs::remove_file(&path).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to delete file: {}", e), + None, + ) + })?; // Remove from active resources if present - if let Ok(url) = Url::from_file_path(path) { + if let Ok(url) = Url::from_file_path(&path) { self.active_resources .lock() .unwrap() .remove(&url.to_string()); } - Ok(vec![Content::text(format!("Deleted file: {}", path))]) + format!("Deleted file: {}", path) } "clear" => { - fs::remove_dir_all(&self.cache_dir).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to clear cache directory: {}", e)), - data: None, - })?; - fs::create_dir_all(&self.cache_dir).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to recreate cache directory: {}", e)), - data: None, - })?; + fs::remove_dir_all(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to clear cache directory: {}", e), + None, + ) + })?; + fs::create_dir_all(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to recreate cache directory: {}", e), + None, + ) + })?; // Clear active resources self.active_resources.lock().unwrap().clear(); - Ok(vec![Content::text("Cache cleared successfully.")]) + "Cache cleared successfully.".to_string() } - _ => Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "Invalid 'command' parameter: {}. Valid options are: 'list', 'view', 'delete', 'clear'", - command)), - data: None, - }), - } - } -} + _ => { + return Err(ErrorData::new(ErrorCode::INVALID_PARAMS, format!( + "Invalid 'command' parameter: {}. Valid options are: 'list', 'view', 'delete', 'clear'", + params.command + ), None)); + } + }; -impl Router for ComputerControllerRouter { - fn name(&self) -> String { - "ComputerControllerExtension".to_string() + Ok(CallToolResult::success(vec![ + Content::text(result).with_audience(vec![Role::Assistant]) + ])) } - fn instructions(&self) -> String { - self.instructions.clone() - } + /// Process PDF files to extract text and images. + /// Supports operations: + /// - extract_text: Extract all text content from the PDF + /// - extract_images: Extract and save embedded images to PNG files + /// + /// Use this when there is a .pdf file or files that need to be processed. + #[tool( + name = "pdf_tool", + description = "Process PDF files to extract text and images." + )] + pub async fn pdf_tool( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let result = pdf_tool::pdf_tool(¶ms.path, ¶ms.operation, &self.cache_dir) + .await + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.message.to_string(), None))?; - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new() - .with_tools(false) - .with_resources(false, false) - .build() + Ok(CallToolResult::success(result)) } - fn list_tools(&self) -> Vec { - self.tools.clone() + /// Process DOCX files to extract text and create/update documents. + /// Supports operations: + /// - extract_text: Extract all text content and structure (headings, TOC) from the DOCX + /// - update_doc: Create a new DOCX or update existing one with provided content + /// Modes: + /// - append: Add content to end of document (default) + /// - replace: Replace specific text with new content + /// - structured: Add content with specific heading level and styling + /// - add_image: Add an image to the document (with optional caption) + /// + /// Use this when there is a .docx file that needs to be processed or created. + #[tool( + name = "docx_tool", + description = "Process DOCX files to extract text and create/update documents." + )] + pub async fn docx_tool( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let result = docx_tool::docx_tool( + ¶ms.path, + ¶ms.operation, + params.content.as_deref(), + params.params.as_ref(), + ) + .await + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.message.to_string(), None))?; + + Ok(CallToolResult::success(result)) } - fn call_tool( + /// Process Excel (XLSX) files to read and manipulate spreadsheet data. + /// Supports operations: + /// - list_worksheets: List all worksheets in the workbook (returns name, index, column_count, row_count) + /// - get_columns: Get column names from a worksheet (returns values from the first row) + /// - get_range: Get values and formulas from a cell range (e.g., "A1:C10") (returns a 2D array organized as [row][column]) + /// - find_text: Search for text in a worksheet (returns a list of (row, column) coordinates) + /// - update_cell: Update a single cell's value (returns confirmation message) + /// - get_cell: Get value and formula from a specific cell (returns both value and formula if present) + /// - save: Save changes back to the file (returns confirmation message) + /// + /// Use this when working with Excel spreadsheets to analyze or modify data. + #[tool( + name = "xlsx_tool", + description = "Process Excel (XLSX) files to read and manipulate spreadsheet data." + )] + pub async fn xlsx_tool( &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - Box::pin(async move { - match tool_name.as_str() { - "web_scrape" => this.web_scrape(arguments).await, - "automation_script" => this.quick_script(arguments).await, - "computer_control" => this.computer_control(arguments).await, - "cache" => this.cache(arguments).await, - "pdf_tool" => this.pdf_tool(arguments).await, - "docx_tool" => this.docx_tool(arguments).await, - "xlsx_tool" => this.xlsx_tool(arguments).await, - _ => Err(ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(format!("Tool {} not found", tool_name)), - data: None, - }), + params: Parameters, + ) -> Result { + let params = params.0; + let result = match params.operation.as_str() { + "list_worksheets" => { + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheets = xlsx + .list_worksheets() + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!("{:#?}", worksheets))] } - }) - } + "get_columns" => { + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + } else { + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + }; + let columns = xlsx + .get_column_names(worksheet) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!("{:#?}", columns))] + } + "get_range" => { + let range = params.range.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'range' parameter".to_string(), + None, + ) + })?; - fn list_resources(&self) -> Vec { - let active_resources = self.active_resources.lock().unwrap(); - let resources = active_resources.values().cloned().collect(); - tracing::info!("Listing resources: {:?}", resources); - resources - } + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + } else { + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + }; + let range_data = xlsx + .get_range(worksheet, &range) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!("{:#?}", range_data))] + } + "find_text" => { + let search_text = params.search_text.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'search_text' parameter".to_string(), + None, + ) + })?; - fn read_resource( - &self, - uri: &str, - ) -> Pin> + Send + 'static>> { - let uri = uri.to_string(); - let this = self.clone(); - - Box::pin(async move { - let active_resources = this.active_resources.lock().unwrap(); - let resource = active_resources - .get(&uri) - .ok_or_else(|| ResourceError::NotFound(format!("Resource not found: {}", uri)))? - .clone(); - - let url = Url::parse(&uri) - .map_err(|e| ResourceError::NotFound(format!("Invalid URI: {}", e)))?; - - if url.scheme() != "file" { - return Err(ResourceError::NotFound( - "Only file:// URIs are supported".into(), - )); + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + } else { + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + }; + let matches = xlsx + .find_in_worksheet(worksheet, &search_text, params.case_sensitive) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!("Found matches at: {:#?}", matches))] } + "update_cell" => { + let row = params.row.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'row' parameter".to_string(), + None, + ) + })?; + let col = params.col.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'col' parameter".to_string(), + None, + ) + })?; + let value = params.value.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'value' parameter".to_string(), + None, + ) + })?; - let path = url - .to_file_path() - .map_err(|_| ResourceError::NotFound("Invalid file path in URI".into()))?; - - match resource.raw.mime_type.as_deref() { - Some("text") | Some("json") | None => fs::read_to_string(&path).map_err(|e| { - ResourceError::ExecutionError(format!("Failed to read file: {}", e)) - }), - Some("binary") => { - let bytes = fs::read(&path).map_err(|e| { - ResourceError::ExecutionError(format!("Failed to read file: {}", e)) - })?; - Ok(base64::prelude::BASE64_STANDARD.encode(bytes)) - } - Some(mime_type) => Err(ResourceError::NotFound(format!( - "Unsupported mime type: {}", - mime_type - ))), + let worksheet_name = params.worksheet.as_deref().unwrap_or("Sheet1"); + + let mut xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + xlsx.update_cell(worksheet_name, row as u32, col as u32, &value) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + xlsx.save(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!( + "Updated cell ({}, {}) to '{}' in worksheet '{}'", + row, col, value, worksheet_name + ))] } - }) - } + "save" => { + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + xlsx.save(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text("File saved successfully.")] + } + "get_cell" => { + let row = params.row.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'row' parameter".to_string(), + None, + ) + })?; + let col = params.col.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'col' parameter".to_string(), + None, + ) + })?; - fn list_prompts(&self) -> Vec { - vec![] - } + let xlsx = xlsx_tool::XlsxTool::new(¶ms.path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + } else { + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) + })? + }; + let cell_value = xlsx + .get_cell_value(worksheet, row as u32, col as u32) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + vec![Content::text(format!("{:#?}", cell_value))] + } + _ => { + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + format!("Invalid operation: {}", params.operation), + None, + )); + } + }; - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) - }) + Ok(CallToolResult::success(result)) } } diff --git a/crates/goose-mcp/src/developer/rmcp_developer.rs b/crates/goose-mcp/src/developer/rmcp_developer.rs index 33c2fe491558..b0772ecef855 100644 --- a/crates/goose-mcp/src/developer/rmcp_developer.rs +++ b/crates/goose-mcp/src/developer/rmcp_developer.rs @@ -548,7 +548,7 @@ impl DeveloperServer { })?; let window_titles: Vec = - windows.into_iter().map(|w| w.title().to_string()).collect(); + windows.into_iter().filter_map(|w| w.title().ok()).collect(); let content_text = format!("Available windows:\n{}", window_titles.join("\n")); @@ -588,7 +588,7 @@ impl DeveloperServer { let window = windows .into_iter() - .find(|w| w.title() == window_title) + .find(|w| w.title().ok().as_ref() == Some(window_title)) .ok_or_else(|| { ErrorData::new( ErrorCode::INTERNAL_ERROR, diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index f780feac0887..b3263f6ae3dd 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -13,8 +13,8 @@ pub mod developer; mod memory; mod tutorial; -pub use autovisualiser::AutoVisualiserRouter; -pub use computercontroller::ComputerControllerRouter; +pub use autovisualiser::AutoVisualiserServer; +pub use computercontroller::ComputerControllerServer; pub use developer::rmcp_developer::DeveloperServer; -pub use memory::MemoryRouter; -pub use tutorial::TutorialRouter; +pub use memory::MemoryServer; +pub use tutorial::TutorialServer; diff --git a/crates/goose-mcp/src/memory/mod.rs b/crates/goose-mcp/src/memory/mod.rs index 2bd1d20a57b0..823b01f7eb39 100644 --- a/crates/goose-mcp/src/memory/mod.rs +++ b/crates/goose-mcp/src/memory/mod.rs @@ -1,129 +1,122 @@ -use async_trait::async_trait; use etcetera::{choose_app_strategy, AppStrategy}; use indoc::formatdoc; -use mcp_core::{ - handler::{PromptError, ResourceError}, - protocol::ServerCapabilities, - tool::ToolCall, +use rmcp::handler::server::wrapper::Parameters; +use rmcp::{ + handler::server::router::tool::ToolRouter, + model::{ + CallToolResult, Content, ErrorCode, ErrorData, Implementation, Role, ServerCapabilities, + ServerInfo, + }, + schemars::JsonSchema, + tool, tool_handler, tool_router, ServerHandler, }; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::{ - Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, Resource, Tool, ToolAnnotations, -}; -use rmcp::object; -use serde_json::Value; +use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs, - future::Future, io::{self, Read, Write}, path::PathBuf, - pin::Pin, }; -use tokio::sync::mpsc; -// MemoryRouter implementation -#[derive(Clone)] -pub struct MemoryRouter { - tools: Vec, +/// Parameters for the remember_memory tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RememberMemoryParams { + /// Category to store the memory in + pub category: String, + /// Memory data to store + pub data: String, + /// Optional tags for the memory + #[serde(default)] + pub tags: Vec, + /// Whether to store globally or locally + pub is_global: bool, +} + +/// Parameters for the retrieve_memories tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RetrieveMemoriesParams { + /// Category to retrieve memories from (use "*" for all categories) + pub category: String, + /// Whether to retrieve from global or local storage + pub is_global: bool, +} + +/// Parameters for the remove_memory_category tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RemoveMemoryCategoryParams { + /// Category to remove (use "*" for all categories) + pub category: String, + /// Whether to remove from global or local storage + pub is_global: bool, +} + +/// Parameters for the remove_specific_memory tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct RemoveSpecificMemoryParams { + /// Category containing the memory to remove + pub category: String, + /// Content of the memory to remove + pub memory_content: String, + /// Whether to remove from global or local storage + pub is_global: bool, +} + +/// Memory MCP Server using official RMCP SDK +#[derive(Debug)] +pub struct MemoryServer { + tool_router: ToolRouter, instructions: String, global_memory_dir: PathBuf, local_memory_dir: PathBuf, } -impl Default for MemoryRouter { +#[tool_handler(router = self.tool_router)] +impl ServerHandler for MemoryServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "goose-memory".to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + capabilities: ServerCapabilities::builder().enable_tools().build(), + instructions: Some(self.instructions.clone()), + ..Default::default() + } + } +} + +impl Default for MemoryServer { fn default() -> Self { Self::new() } } -impl MemoryRouter { +#[tool_router(router = tool_router)] +impl MemoryServer { + #[allow(clippy::too_many_lines)] pub fn new() -> Self { - let remember_memory = Tool::new( - "remember_memory", - "Stores a memory with optional tags in a specified category", - object!({ - "type": "object", - "properties": { - "category": {"type": "string"}, - "data": {"type": "string"}, - "tags": {"type": "array", "items": {"type": "string"}}, - "is_global": {"type": "boolean"} - }, - "required": ["category", "data", "is_global"] - }), - ) - .annotate(ToolAnnotations { - title: Some("Remember Memory".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(true), - open_world_hint: Some(false), - }); + let instructions = Self::create_base_instructions(); - let retrieve_memories = Tool::new( - "retrieve_memories", - "Retrieves all memories from a specified category", - object!({ - "type": "object", - "properties": { - "category": {"type": "string"}, - "is_global": {"type": "boolean"} - }, - "required": ["category", "is_global"] - }), - ) - .annotate(ToolAnnotations { - title: Some("Retrieve Memory".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); + // Setup directories + let (global_memory_dir, local_memory_dir) = Self::setup_memory_directories(); - let remove_memory_category = Tool::new( - "remove_memory_category", - "Removes all memories within a specified category", - object!({ - "type": "object", - "properties": { - "category": {"type": "string"}, - "is_global": {"type": "boolean"} - }, - "required": ["category", "is_global"] - }), - ) - .annotate(ToolAnnotations { - title: Some("Remove Memory Category".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); + let mut memory_server = Self { + tool_router: Self::tool_router(), + instructions: instructions.clone(), + global_memory_dir, + local_memory_dir, + }; - let remove_specific_memory = Tool::new( - "remove_specific_memory", - "Removes a specific memory within a specified category", - object!({ - "type": "object", - "properties": { - "category": {"type": "string"}, - "memory_content": {"type": "string"}, - "is_global": {"type": "boolean"} - }, - "required": ["category", "memory_content", "is_global"] - }), - ) - .annotate(ToolAnnotations { - title: Some("Remove Specific Memory".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(true), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); + // Load existing memories and update instructions + memory_server.instructions = + Self::update_instructions_with_memories(&memory_server, instructions); - let instructions = formatdoc! {r#" + memory_server + } + + #[allow(clippy::too_many_lines)] + fn create_base_instructions() -> String { + formatdoc! {r#" This extension allows storage and retrieval of categorized information with tagging support. It's designed to help manage important information across sessions in a systematic and organized manner. Capabilities: @@ -224,8 +217,10 @@ impl MemoryRouter { - Propose suitable categories and tag suggestions. - Discuss storage scope thoroughly to align with user needs. - Acknowledge the user about what is stored and where, for transparency and ease of future retrieval. - "#}; + "#} + } + fn setup_memory_directories() -> (PathBuf, PathBuf) { // Check for .goose/memory in current directory let local_memory_dir = std::env::var("GOOSE_WORKING_DIR") .map(PathBuf::from) @@ -241,22 +236,17 @@ impl MemoryRouter { .map(|strategy| strategy.in_config_dir("memory")) .unwrap_or_else(|_| PathBuf::from(".config/goose/memory")); - let mut memory_router = Self { - tools: vec![ - remember_memory, - retrieve_memories, - remove_memory_category, - remove_specific_memory, - ], - instructions: instructions.clone(), - global_memory_dir, - local_memory_dir, - }; + (global_memory_dir, local_memory_dir) + } - let retrieved_global_memories = memory_router.retrieve_all(true); - let retrieved_local_memories = memory_router.retrieve_all(false); + fn update_instructions_with_memories( + memory_server: &MemoryServer, + base_instructions: String, + ) -> String { + let retrieved_global_memories = memory_server.retrieve_all(true); + let retrieved_local_memories = memory_server.retrieve_all(false); - let mut updated_instructions = instructions; + let mut updated_instructions = base_instructions; let memories_follow_up_instructions = formatdoc! {r#" **Here are the user's currently saved memories:** @@ -293,22 +283,156 @@ impl MemoryRouter { } } - memory_router.set_instructions(updated_instructions); + updated_instructions + } + + /// Stores a memory with optional tags in a specified category + #[tool( + name = "remember_memory", + description = "Stores a memory with optional tags in a specified category" + )] + pub async fn remember_memory( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + + if params.data.is_empty() { + return Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Data must exist when remembering a memory".to_string(), + None, + )); + } + + let tags: Vec<&str> = params.tags.iter().map(|s| s.as_str()).collect(); + self.remember( + "context", + ¶ms.category, + ¶ms.data, + &tags, + params.is_global, + ) + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to store memory: {}", e), + None, + ) + })?; - memory_router + Ok(CallToolResult::success(vec![Content::text(format!( + "Stored memory in category: {}", + params.category + )) + .with_audience(vec![Role::Assistant])])) } - // Add a setter method for instructions - pub fn set_instructions(&mut self, new_instructions: String) { - self.instructions = new_instructions; + /// Retrieves all memories from a specified category + #[tool( + name = "retrieve_memories", + description = "Retrieves all memories from a specified category" + )] + pub async fn retrieve_memories( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + + let memories = if params.category == "*" { + self.retrieve_all(params.is_global) + } else { + self.retrieve(¶ms.category, params.is_global) + } + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to retrieve memories: {}", e), + None, + ) + })?; + + let result = format!("Retrieved memories: {:?}", memories); + + Ok(CallToolResult::success(vec![ + Content::text(result).with_audience(vec![Role::Assistant]) + ])) } - pub fn get_instructions(&self) -> &str { - &self.instructions + /// Removes all memories within a specified category + #[tool( + name = "remove_memory_category", + description = "Removes all memories within a specified category" + )] + pub async fn remove_memory_category( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + + let result = if params.category == "*" { + self.clear_all_global_or_local_memories(params.is_global) + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to clear all memories: {}", e), + None, + ) + })?; + format!( + "Cleared all memory {} categories", + if params.is_global { "global" } else { "local" } + ) + } else { + self.clear_memory(¶ms.category, params.is_global) + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to clear memory category: {}", e), + None, + ) + })?; + format!("Cleared memories in category: {}", params.category) + }; + + Ok(CallToolResult::success(vec![ + Content::text(result).with_audience(vec![Role::Assistant]) + ])) } + /// Removes a specific memory within a specified category + #[tool( + name = "remove_specific_memory", + description = "Removes a specific memory within a specified category" + )] + pub async fn remove_specific_memory( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + + self.remove_specific_memory_impl( + ¶ms.category, + ¶ms.memory_content, + params.is_global, + ) + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to remove specific memory: {}", e), + None, + ) + })?; + + Ok(CallToolResult::success(vec![Content::text(format!( + "Removed specific memory from category: {}", + params.category + )) + .with_audience(vec![Role::Assistant])])) + } + + // Helper methods fn get_memory_file(&self, category: &str, is_global: bool) -> PathBuf { - // Defaults to local memory if no is_global flag is provided let base_dir = if is_global { &self.global_memory_dir } else { @@ -317,7 +441,7 @@ impl MemoryRouter { base_dir.join(format!("{}.txt", category)) } - pub fn retrieve_all(&self, is_global: bool) -> io::Result>> { + fn retrieve_all(&self, is_global: bool) -> io::Result>> { let base_dir = if is_global { &self.global_memory_dir } else { @@ -340,7 +464,7 @@ impl MemoryRouter { Ok(memories) } - pub fn remember( + fn remember( &self, _context: &str, category: &str, @@ -366,7 +490,7 @@ impl MemoryRouter { Ok(()) } - pub fn retrieve( + fn retrieve( &self, category: &str, is_global: bool, @@ -405,7 +529,7 @@ impl MemoryRouter { Ok(memories) } - pub fn remove_specific_memory( + fn remove_specific_memory_impl( &self, category: &str, memory_content: &str, @@ -432,7 +556,7 @@ impl MemoryRouter { Ok(()) } - pub fn clear_memory(&self, category: &str, is_global: bool) -> io::Result<()> { + fn clear_memory(&self, category: &str, is_global: bool) -> io::Result<()> { let memory_file_path = self.get_memory_file(category, is_global); if memory_file_path.exists() { fs::remove_file(memory_file_path)?; @@ -441,7 +565,7 @@ impl MemoryRouter { Ok(()) } - pub fn clear_all_global_or_local_memories(&self, is_global: bool) -> io::Result<()> { + fn clear_all_global_or_local_memories(&self, is_global: bool) -> io::Result<()> { let base_dir = if is_global { &self.global_memory_dir } else { @@ -452,176 +576,16 @@ impl MemoryRouter { } Ok(()) } - - async fn execute_tool_call(&self, tool_call: ToolCall) -> Result { - match tool_call.name.as_str() { - "remember_memory" => { - let args = MemoryArgs::from_value(&tool_call.arguments)?; - let data = args.data.filter(|d| !d.is_empty()).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "Data must exist when remembering a memory", - ) - })?; - self.remember("context", args.category, data, &args.tags, args.is_global)?; - Ok(format!("Stored memory in category: {}", args.category)) - } - "retrieve_memories" => { - let args = MemoryArgs::from_value(&tool_call.arguments)?; - let memories = if args.category == "*" { - self.retrieve_all(args.is_global)? - } else { - self.retrieve(args.category, args.is_global)? - }; - Ok(format!("Retrieved memories: {:?}", memories)) - } - "remove_memory_category" => { - let args = MemoryArgs::from_value(&tool_call.arguments)?; - if args.category == "*" { - self.clear_all_global_or_local_memories(args.is_global)?; - Ok(format!( - "Cleared all memory {} categories", - if args.is_global { "global" } else { "local" } - )) - } else { - self.clear_memory(args.category, args.is_global)?; - Ok(format!("Cleared memories in category: {}", args.category)) - } - } - "remove_specific_memory" => { - let args = MemoryArgs::from_value(&tool_call.arguments)?; - let memory_content = tool_call.arguments["memory_content"].as_str().unwrap(); - self.remove_specific_memory(args.category, memory_content, args.is_global)?; - Ok(format!( - "Removed specific memory from category: {}", - args.category - )) - } - _ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Unknown tool")), - } - } } -#[async_trait] -impl Router for MemoryRouter { - fn name(&self) -> String { - "memory".to_string() - } - - fn instructions(&self) -> String { - self.instructions.clone() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new().with_tools(false).build() - } - - fn list_tools(&self) -> Vec { - self.tools.clone() - } - - fn call_tool( - &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - - Box::pin(async move { - let tool_call = ToolCall { - name: tool_name, - arguments, - }; - match this.execute_tool_call(tool_call).await { - Ok(result) => Ok(vec![Content::text(result)]), - Err(err) => Err(ErrorData::new( - ErrorCode::INTERNAL_ERROR, - err.to_string(), - None, - )), - } - }) - } - - fn list_resources(&self) -> Vec { - Vec::new() - } - - fn read_resource( - &self, - _uri: &str, - ) -> Pin> + Send + 'static>> { - Box::pin(async move { Ok("".to_string()) }) - } - fn list_prompts(&self) -> Vec { - vec![] - } - - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) - }) - } -} - -#[derive(Debug)] -struct MemoryArgs<'a> { - category: &'a str, - data: Option<&'a str>, - tags: Vec<&'a str>, - is_global: bool, -} - -impl<'a> MemoryArgs<'a> { - // Category is required, data is optional, tags are optional, is_global is optional - fn from_value(args: &'a Value) -> Result { - let category = args["category"].as_str().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "Category must be a string") - })?; - - if category.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Category must be a string", - )); +impl Clone for MemoryServer { + fn clone(&self) -> Self { + Self { + tool_router: Self::tool_router(), + instructions: self.instructions.clone(), + global_memory_dir: self.global_memory_dir.clone(), + local_memory_dir: self.local_memory_dir.clone(), } - - let data = args.get("data").and_then(|d| d.as_str()); - - let tags = match &args["tags"] { - Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect(), - Value::String(s) => vec![s.as_str()], - _ => Vec::new(), - }; - - let is_global = match &args.get("is_global") { - // Default to false if no is_global flag is provided - Some(Value::Bool(b)) => *b, - Some(Value::String(s)) => s.to_lowercase() == "true", - None => false, - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "is_global must be a boolean or string 'true'/'false'", - )) - } - }; - - Ok(Self { - category, - data, - tags, - is_global, - }) } } @@ -635,17 +599,17 @@ mod tests { let temp_dir = tempdir().unwrap(); let memory_base = temp_dir.path().join("test_memory"); - let router = MemoryRouter { - tools: vec![], + let server = MemoryServer { + tool_router: MemoryServer::tool_router(), instructions: String::new(), global_memory_dir: memory_base.join("global"), local_memory_dir: memory_base.join("local"), }; - assert!(!router.global_memory_dir.exists()); - assert!(!router.local_memory_dir.exists()); + assert!(!server.global_memory_dir.exists()); + assert!(!server.local_memory_dir.exists()); - router + server .remember( "test_context", "test_category", @@ -655,10 +619,10 @@ mod tests { ) .unwrap(); - assert!(router.local_memory_dir.exists()); - assert!(!router.global_memory_dir.exists()); + assert!(server.local_memory_dir.exists()); + assert!(!server.global_memory_dir.exists()); - router + server .remember( "test_context", "global_category", @@ -668,7 +632,7 @@ mod tests { ) .unwrap(); - assert!(router.global_memory_dir.exists()); + assert!(server.global_memory_dir.exists()); } #[test] @@ -676,15 +640,15 @@ mod tests { let temp_dir = tempdir().unwrap(); let memory_base = temp_dir.path().join("nonexistent_memory"); - let router = MemoryRouter { - tools: vec![], + let server = MemoryServer { + tool_router: MemoryServer::tool_router(), instructions: String::new(), global_memory_dir: memory_base.join("global"), local_memory_dir: memory_base.join("local"), }; - assert!(router.clear_all_global_or_local_memories(false).is_ok()); - assert!(router.clear_all_global_or_local_memories(true).is_ok()); + assert!(server.clear_all_global_or_local_memories(false).is_ok()); + assert!(server.clear_all_global_or_local_memories(true).is_ok()); } #[test] @@ -692,14 +656,14 @@ mod tests { let temp_dir = tempdir().unwrap(); let memory_base = temp_dir.path().join("workflow_test"); - let router = MemoryRouter { - tools: vec![], + let server = MemoryServer { + tool_router: MemoryServer::tool_router(), instructions: String::new(), global_memory_dir: memory_base.join("global"), local_memory_dir: memory_base.join("local"), }; - router + server .remember( "context", "test_category", @@ -709,7 +673,7 @@ mod tests { ) .unwrap(); - let memories = router.retrieve("test_category", false).unwrap(); + let memories = server.retrieve("test_category", false).unwrap(); assert!(!memories.is_empty()); let has_content = memories.values().any(|v| { @@ -718,9 +682,9 @@ mod tests { }); assert!(has_content); - router.clear_memory("test_category", false).unwrap(); + server.clear_memory("test_category", false).unwrap(); - let memories_after_clear = router.retrieve("test_category", false).unwrap(); + let memories_after_clear = server.retrieve("test_category", false).unwrap(); assert!(memories_after_clear.is_empty()); } @@ -729,21 +693,21 @@ mod tests { let temp_dir = tempdir().unwrap(); let memory_base = temp_dir.path().join("write_test"); - let router = MemoryRouter { - tools: vec![], + let server = MemoryServer { + tool_router: MemoryServer::tool_router(), instructions: String::new(), global_memory_dir: memory_base.join("global"), local_memory_dir: memory_base.join("local"), }; - assert!(!router.local_memory_dir.exists()); + assert!(!server.local_memory_dir.exists()); - router + server .remember("context", "category", "data", &[], false) .unwrap(); - assert!(router.local_memory_dir.exists()); - assert!(router.local_memory_dir.join("category.txt").exists()); + assert!(server.local_memory_dir.exists()); + assert!(server.local_memory_dir.join("category.txt").exists()); } #[test] @@ -751,28 +715,28 @@ mod tests { let temp_dir = tempdir().unwrap(); let memory_base = temp_dir.path().join("remove_test"); - let router = MemoryRouter { - tools: vec![], + let server = MemoryServer { + tool_router: MemoryServer::tool_router(), instructions: String::new(), global_memory_dir: memory_base.join("global"), local_memory_dir: memory_base.join("local"), }; - router + server .remember("context", "category", "keep_this", &[], false) .unwrap(); - router + server .remember("context", "category", "remove_this", &[], false) .unwrap(); - let memories = router.retrieve("category", false).unwrap(); + let memories = server.retrieve("category", false).unwrap(); assert_eq!(memories.len(), 1); - router - .remove_specific_memory("category", "remove_this", false) + server + .remove_specific_memory_impl("category", "remove_this", false) .unwrap(); - let memories_after = router.retrieve("category", false).unwrap(); + let memories_after = server.retrieve("category", false).unwrap(); let has_removed = memories_after .values() .any(|v| v.iter().any(|content| content.contains("remove_this"))); diff --git a/crates/goose-mcp/src/tutorial/mod.rs b/crates/goose-mcp/src/tutorial/mod.rs index 0b579085caf8..c8c592795b1e 100644 --- a/crates/goose-mcp/src/tutorial/mod.rs +++ b/crates/goose-mcp/src/tutorial/mod.rs @@ -1,57 +1,59 @@ use anyhow::Result; use include_dir::{include_dir, Dir}; use indoc::formatdoc; -use mcp_core::{ - handler::{PromptError, ResourceError}, - protocol::ServerCapabilities, +use rmcp::handler::server::wrapper::Parameters; +use rmcp::{ + handler::server::router::tool::ToolRouter, + model::{ + CallToolResult, Content, ErrorCode, ErrorData, Implementation, Role, ServerCapabilities, + ServerInfo, + }, + schemars::JsonSchema, + tool, tool_handler, tool_router, ServerHandler, }; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::{ - Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, Resource, Role, Tool, ToolAnnotations, -}; -use rmcp::object; -use serde_json::Value; -use std::{future::Future, pin::Pin}; -use tokio::sync::mpsc; +use serde::{Deserialize, Serialize}; static TUTORIALS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/tutorial/tutorials"); -pub struct TutorialRouter { - tools: Vec, +/// Parameters for the load_tutorial tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct LoadTutorialParams { + /// Name of the tutorial to load, e.g. 'getting-started' or 'developer-mcp' + pub name: String, +} + +/// Tutorial MCP Server using official RMCP SDK +#[derive(Debug)] +pub struct TutorialServer { + tool_router: ToolRouter, instructions: String, } -impl Default for TutorialRouter { +#[tool_handler(router = self.tool_router)] +impl ServerHandler for TutorialServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "goose-tutorial".to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + capabilities: ServerCapabilities::builder().enable_tools().build(), + instructions: Some(self.instructions.clone()), + ..Default::default() + } + } +} + +impl Default for TutorialServer { fn default() -> Self { Self::new() } } -impl TutorialRouter { +#[tool_router(router = tool_router)] +impl TutorialServer { pub fn new() -> Self { - let load_tutorial = Tool::new( - "load_tutorial".to_string(), - "Load a specific tutorial by name. The tutorial will be returned as markdown content that provides step by step instructions.".to_string(), - object!({ - "type": "object", - "required": ["name"], - "properties": { - "name": { - "type": "string", - "description": "Name of the tutorial to load, e.g. 'getting-started' or 'developer-mcp'" - } - } - }) - ).annotate(ToolAnnotations { - title: Some("Load Tutorial".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }); - - // Get base instructions and available tutorials + // Get available tutorials let available_tutorials = Self::get_available_tutorials(); let instructions = formatdoc! {r#" @@ -73,11 +75,31 @@ impl TutorialRouter { }; Self { - tools: vec![load_tutorial], + tool_router: Self::tool_router(), instructions, } } + /// Load a specific tutorial by name. The tutorial will be returned as markdown content that provides step by step instructions. + #[tool( + name = "load_tutorial", + description = "Load a specific tutorial by name. The tutorial will be returned as markdown content that provides step by step instructions." + )] + pub async fn load_tutorial( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let name = ¶ms.name; + + let content = self.load_tutorial_content(name).await?; + + Ok(CallToolResult::success(vec![ + Content::text(content).with_audience(vec![Role::Assistant]) + ])) + } + + /// Get list of available tutorials with descriptions fn get_available_tutorials() -> String { let mut tutorials = String::new(); for file in TUTORIALS_DIR.files() { @@ -94,7 +116,8 @@ impl TutorialRouter { tutorials } - async fn load_tutorial(&self, name: &str) -> Result { + /// Load tutorial content by name + async fn load_tutorial_content(&self, name: &str) -> Result { let file_name = format!("{}.md", name); let file = TUTORIALS_DIR.get_file(&file_name).ok_or(ErrorData::new( ErrorCode::INTERNAL_ERROR, @@ -105,93 +128,10 @@ impl TutorialRouter { } } -impl Router for TutorialRouter { - fn name(&self) -> String { - "tutorial".to_string() - } - - fn instructions(&self) -> String { - self.instructions.clone() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new().with_tools(false).build() - } - - fn list_tools(&self) -> Vec { - self.tools.clone() - } - - fn call_tool( - &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - - Box::pin(async move { - match tool_name.as_str() { - "load_tutorial" => { - let name = arguments - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'name' parameter".to_string(), - None, - ) - })?; - - let content = this.load_tutorial(name).await?; - Ok(vec![ - Content::text(content).with_audience(vec![Role::Assistant]) - ]) - } - _ => Err(ErrorData::new( - ErrorCode::RESOURCE_NOT_FOUND, - format!("Tool {} not found", tool_name), - None, - )), - } - }) - } - - fn list_resources(&self) -> Vec { - Vec::new() - } - - fn read_resource( - &self, - _uri: &str, - ) -> Pin> + Send + 'static>> { - Box::pin(async move { Ok("".to_string()) }) - } - - fn list_prompts(&self) -> Vec { - vec![] - } - - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) - }) - } -} - -impl Clone for TutorialRouter { +impl Clone for TutorialServer { fn clone(&self) -> Self { Self { - tools: self.tools.clone(), + tool_router: Self::tool_router(), instructions: self.instructions.clone(), } } diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index de4dbba76ccb..4b84ecd8c32e 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -14,7 +14,6 @@ workspace = true goose = { path = "../goose" } mcp-core = { path = "../mcp-core" } goose-mcp = { path = "../goose-mcp" } -mcp-server = { path = "../mcp-server" } rmcp = { workspace = true } schemars = "1.0" axum = { version = "0.8.1", features = ["ws", "macros"] } diff --git a/crates/goose-server/src/commands/mcp.rs b/crates/goose-server/src/commands/mcp.rs index b1bed12ed210..3e4ad215a467 100644 --- a/crates/goose-server/src/commands/mcp.rs +++ b/crates/goose-server/src/commands/mcp.rs @@ -1,11 +1,8 @@ use anyhow::{anyhow, Result}; use goose_mcp::{ - AutoVisualiserRouter, ComputerControllerRouter, DeveloperServer, MemoryRouter, TutorialRouter, + AutoVisualiserServer, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, }; -use mcp_server::router::RouterService; -use mcp_server::{BoundedService, ByteTransport, Server}; use rmcp::{transport::stdio, ServiceExt}; -use tokio::io::{stdin, stdout}; pub async fn run(name: &str) -> Result<()> { crate::logging::setup_logging(Some(&format!("mcp-{name}")))?; @@ -29,17 +26,51 @@ pub async fn run(name: &str) -> Result<()> { service.waiting().await?; return Ok(()); } - let router: Option> = match name { - "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - "autovisualiser" => Some(Box::new(RouterService(AutoVisualiserRouter::new()))), - "memory" => Some(Box::new(RouterService(MemoryRouter::new()))), - "tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))), - _ => None, - }; - - let server = Server::new(router.unwrap_or_else(|| panic!("Unknown server requested {}", name))); - let transport = ByteTransport::new(stdin(), stdout()); - - tracing::info!("Server initialized and ready to handle requests"); - Ok(server.run(transport).await?) + + if name == "tutorial" { + let service = TutorialServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + return Ok(()); + } + + if name == "memory" { + let service = MemoryServer::new().serve(stdio()).await.inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + return Ok(()); + } + + if name == "computercontroller" { + let service = ComputerControllerServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + return Ok(()); + } + + if name == "autovisualiser" { + let service = AutoVisualiserServer::new() + .serve(stdio()) + .await + .inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + return Ok(()); + } + + panic!("Unknown server requested {}", name); } diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index e96e3e30e8b9..a3e57126bba1 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -99,7 +99,6 @@ ahash = "0.8" tokio-util = "0.7.15" unicode-normalization = "0.1" -arrow = "52.2" oauth2 = "5.0.0" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/mcp-server/Cargo.toml b/crates/mcp-server/Cargo.toml deleted file mode 100644 index 38298372744f..000000000000 --- a/crates/mcp-server/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "mcp-server" -version = "0.1.0" -edition = "2021" - -[lints] -workspace = true - -[dependencies] -anyhow = "1.0.94" -thiserror = "1.0" -mcp-core = { path = "../mcp-core" } -rmcp = { workspace = true } -serde = { version = "1.0.216", features = ["derive"] } -serde_json = "1.0.133" -tokio = { version = "1", features = ["full"] } -tower = { version = "0.4", features = ["timeout"] } -tower-service = "0.3" -futures = "0.3" -pin-project = "1.1" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-appender = "0.2" diff --git a/crates/mcp-server/README.md b/crates/mcp-server/README.md deleted file mode 100644 index 1e4f06176553..000000000000 --- a/crates/mcp-server/README.md +++ /dev/null @@ -1,7 +0,0 @@ -### Test with MCP Inspector - -```bash -npx @modelcontextprotocol/inspector cargo run -p mcp-server -``` - -Then visit the Inspector in the browser window and test the different endpoints. \ No newline at end of file diff --git a/crates/mcp-server/src/errors.rs b/crates/mcp-server/src/errors.rs deleted file mode 100644 index d4cd59e363d7..000000000000 --- a/crates/mcp-server/src/errors.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::borrow::Cow; - -use thiserror::Error; - -pub type BoxError = Box; - -#[derive(Error, Debug)] -pub enum TransportError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("JSON serialization error: {0}")] - Json(#[from] serde_json::Error), - - #[error("Invalid UTF-8 sequence: {0}")] - Utf8(#[from] std::string::FromUtf8Error), - - #[error("Protocol error: {0}")] - Protocol(String), - - #[error("Invalid message format: {0}")] - InvalidMessage(String), -} - -#[derive(Error, Debug)] -pub enum ServerError { - #[error("Transport error: {0}")] - Transport(#[from] TransportError), - - #[error("Service error: {0}")] - Service(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error("Request timed out")] - Timeout(#[from] tower::timeout::error::Elapsed), -} - -#[derive(Error, Debug)] -pub enum RouterError { - #[error("Method not found: {0}")] - MethodNotFound(String), - - #[error("Invalid parameters: {0}")] - InvalidParams(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error("Tool not found: {0}")] - ToolNotFound(String), - - #[error("Resource not found: {0}")] - ResourceNotFound(String), - - #[error("Not found: {0}")] - PromptNotFound(String), -} - -impl From for rmcp::model::ErrorData { - fn from(err: RouterError) -> Self { - use rmcp::model::*; - match err { - RouterError::MethodNotFound(msg) => ErrorData { - code: ErrorCode::METHOD_NOT_FOUND, - message: Cow::from(msg), - data: None, - }, - RouterError::InvalidParams(msg) => ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(msg), - data: None, - }, - RouterError::Internal(msg) => ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(msg), - data: None, - }, - RouterError::ToolNotFound(msg) => ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(msg), - data: None, - }, - RouterError::ResourceNotFound(msg) => ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(msg), - data: None, - }, - RouterError::PromptNotFound(msg) => ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(msg), - data: None, - }, - } - } -} - -impl From for RouterError { - fn from(err: mcp_core::handler::ResourceError) -> Self { - match err { - mcp_core::handler::ResourceError::NotFound(msg) => RouterError::ResourceNotFound(msg), - _ => RouterError::Internal("Unknown resource error".to_string()), - } - } -} diff --git a/crates/mcp-server/src/lib.rs b/crates/mcp-server/src/lib.rs deleted file mode 100644 index 4b4fd891082d..000000000000 --- a/crates/mcp-server/src/lib.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::{ - pin::Pin, - task::{Context, Poll}, -}; - -use futures::{Future, Stream}; -use pin_project::pin_project; -use rmcp::model::{ - ErrorData, JsonRpcError, JsonRpcMessage, JsonRpcResponse, JsonRpcVersion2_0, RequestId, -}; -use router::McpRequest; -use tokio::{ - io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}, - sync::mpsc, -}; -use tower_service::Service; - -mod errors; -pub use errors::{BoxError, RouterError, ServerError, TransportError}; - -pub mod router; -pub use router::Router; - -/// A transport layer that handles JSON-RPC messages over byte -#[pin_project] -pub struct ByteTransport { - // Reader is a BufReader on the underlying stream (stdin or similar) buffering - // the underlying data across poll calls, we clear one line (\n) during each - // iteration of poll_next from this buffer - #[pin] - reader: BufReader, - #[pin] - writer: W, -} - -impl ByteTransport -where - R: AsyncRead, - W: AsyncWrite, -{ - pub fn new(reader: R, writer: W) -> Self { - Self { - // Default BufReader capacity is 8 * 1024, increase this to 2MB to the file size limit - // allows the buffer to have the capacity to read very large calls - reader: BufReader::with_capacity(2 * 1024 * 1024, reader), - writer, - } - } -} - -impl Stream for ByteTransport -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut this = self.project(); - let mut buf = Vec::new(); - - let mut reader = this.reader.as_mut(); - let mut read_future = Box::pin(reader.read_until(b'\n', &mut buf)); - match read_future.as_mut().poll(cx) { - Poll::Ready(Ok(0)) => Poll::Ready(None), // EOF - Poll::Ready(Ok(_)) => { - // Convert to UTF-8 string - let line = match String::from_utf8(buf) { - Ok(s) => s, - Err(e) => return Poll::Ready(Some(Err(TransportError::Utf8(e)))), - }; - // Log incoming message here before serde conversion to - // track incomplete chunks which are not valid JSON - tracing::info!(json = %line, "incoming message"); - - // Parse JSON and validate message format - match serde_json::from_str::(&line) { - Ok(value) => { - // Validate basic JSON-RPC structure - if !value.is_object() { - return Poll::Ready(Some(Err(TransportError::InvalidMessage( - "Message must be a JSON object".into(), - )))); - } - let obj = value.as_object().unwrap(); // Safe due to check above - - // Check jsonrpc version field - if !obj.contains_key("jsonrpc") || obj["jsonrpc"] != "2.0" { - return Poll::Ready(Some(Err(TransportError::InvalidMessage( - "Missing or invalid jsonrpc version".into(), - )))); - } - - // Now try to parse as proper message - match serde_json::from_value::(value) { - Ok(msg) => Poll::Ready(Some(Ok(msg))), - Err(e) => Poll::Ready(Some(Err(TransportError::Json(e)))), - } - } - Err(e) => Poll::Ready(Some(Err(TransportError::Json(e)))), - } - } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(TransportError::Io(e)))), - Poll::Pending => Poll::Pending, - } - } -} - -impl ByteTransport -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - pub async fn write_message(&mut self, msg: JsonRpcMessage) -> Result<(), std::io::Error> { - let json = serde_json::to_string(&msg)?; - Pin::new(&mut self.writer) - .write_all(json.as_bytes()) - .await?; - Pin::new(&mut self.writer).write_all(b"\n").await?; - Pin::new(&mut self.writer).flush().await?; - Ok(()) - } -} - -/// The main server type that processes incoming requests -pub struct Server { - service: S, -} - -impl Server -where - S: Service + Send, - S::Error: Into, - S::Future: Send, -{ - pub fn new(service: S) -> Self { - Self { service } - } - - // TODO transport trait instead of byte transport if we implement others - pub async fn run(self, mut transport: ByteTransport) -> Result<(), ServerError> - where - R: AsyncRead + Unpin + Send + 'static, - W: AsyncWrite + Unpin + Send + 'static, - { - use futures::StreamExt; - let mut service = self.service; - - tracing::info!("Server started"); - while let Some(msg_result) = transport.next().await { - let _span = tracing::span!(tracing::Level::INFO, "message_processing").entered(); - match msg_result { - Ok(msg) => { - match msg { - JsonRpcMessage::Request(request) => { - let request_json = serde_json::to_string(&request) - .unwrap_or_else(|_| "Failed to serialize request".to_string()); - - tracing::info!( - method = ?request.request.method, - json = %request_json, - "Received request" - ); - - // Process the request using our service - let (notify_tx, mut notify_rx) = mpsc::channel(256); - let mcp_request = McpRequest { - request, - notifier: notify_tx, - }; - - let transport_fut = tokio::spawn(async move { - while let Some(notification) = notify_rx.recv().await { - if transport.write_message(notification).await.is_err() { - break; - } - } - transport - }); - - let response = match service.call(mcp_request).await { - Ok(resp) => resp, - Err(e) => { - let error_msg = e.into().to_string(); - tracing::error!(error = %error_msg, "Request processing failed"); - - // Return an error response instead of a regular response - return Err(ServerError::Transport(TransportError::Protocol( - error_msg, - ))); - } - }; - - transport = match transport_fut.await { - Ok(transport) => transport, - Err(e) => { - tracing::error!(error = %e, "Failed to spawn transport task"); - return Err(ServerError::Transport(TransportError::Io( - e.into(), - ))); - } - }; - - // Serialize response for logging - let response_json = serde_json::to_string(&response) - .unwrap_or_else(|_| "Failed to serialize response".to_string()); - - tracing::info!( - response_id = ?response.id, - json = %response_json, - "Sending response" - ); - // Send the response back - if let Err(e) = transport - .write_message(JsonRpcMessage::Response(response)) - .await - { - return Err(ServerError::Transport(TransportError::Io(e))); - } - } - JsonRpcMessage::Response(_) - | JsonRpcMessage::Notification(_) - | JsonRpcMessage::Error(_) => { - // Ignore responses, notifications, batch messages and error messages for now - continue; - } - } - } - Err(e) => { - // Convert transport error to JSON-RPC error response - let error_data = match e { - TransportError::Json(_) | TransportError::InvalidMessage(_) => ErrorData { - code: rmcp::model::ErrorCode::PARSE_ERROR, - message: e.to_string().into(), - data: None, - }, - TransportError::Protocol(_) => ErrorData { - code: rmcp::model::ErrorCode::INVALID_REQUEST, - message: e.to_string().into(), - data: None, - }, - _ => ErrorData { - code: rmcp::model::ErrorCode::INTERNAL_ERROR, - message: e.to_string().into(), - data: None, - }, - }; - - let error_response = JsonRpcMessage::Error(JsonRpcError { - jsonrpc: JsonRpcVersion2_0, - id: RequestId::Number(0), // Use a default ID for transport errors - error: error_data, - }); - - if let Err(e) = transport.write_message(error_response).await { - return Err(ServerError::Transport(TransportError::Io(e))); - } - } - } - } - - Ok(()) - } -} - -// Define a specific service implementation that we need for any -// Any router implements this -pub trait BoundedService: - Service< - McpRequest, - Response = JsonRpcResponse, - Error = BoxError, - Future = Pin> + Send>>, - > + Send - + 'static -{ -} - -// Implement it for any type that meets the bounds -impl BoundedService for T where - T: Service< - McpRequest, - Response = JsonRpcResponse, - Error = BoxError, - Future = Pin> + Send>>, - > + Send - + 'static -{ -} diff --git a/crates/mcp-server/src/main.rs b/crates/mcp-server/src/main.rs deleted file mode 100644 index dd2cdfcc8171..000000000000 --- a/crates/mcp-server/src/main.rs +++ /dev/null @@ -1,248 +0,0 @@ -use anyhow::Result; -use mcp_core::handler::{PromptError, ResourceError}; -use mcp_core::protocol::ServerCapabilities; -use mcp_server::router::{CapabilitiesBuilder, RouterService}; -use mcp_server::{ByteTransport, Router, Server}; -use rmcp::model::{ - Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, PromptArgument, RawResource, Resource, - Tool, ToolAnnotations, -}; -use rmcp::object; -use serde_json::Value; -use std::borrow::Cow; -use std::{future::Future, pin::Pin, sync::Arc}; -use tokio::sync::mpsc; -use tokio::{ - io::{stdin, stdout}, - sync::Mutex, -}; -use tracing_appender::rolling::{RollingFileAppender, Rotation}; -use tracing_subscriber::{self, EnvFilter}; - -// A simple counter service that demonstrates the Router trait -#[derive(Clone)] -struct CounterRouter { - counter: Arc>, -} - -impl CounterRouter { - fn new() -> Self { - Self { - counter: Arc::new(Mutex::new(0)), - } - } - - async fn increment(&self) -> Result { - let mut counter = self.counter.lock().await; - *counter += 1; - Ok(*counter) - } - - async fn decrement(&self) -> Result { - let mut counter = self.counter.lock().await; - *counter -= 1; - Ok(*counter) - } - - async fn get_value(&self) -> Result { - let counter = self.counter.lock().await; - Ok(*counter) - } - - fn _create_resource_text(&self, uri: &str, name: &str) -> Resource { - Resource::new(RawResource::new(uri, name), None) - } -} - -impl Router for CounterRouter { - fn name(&self) -> String { - "counter".to_string() - } - - fn instructions(&self) -> String { - "This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new() - .with_tools(false) - .with_resources(false, false) - .with_prompts(false) - .build() - } - - fn list_tools(&self) -> Vec { - vec![ - Tool::new( - "increment".to_string(), - "Increment the counter by 1".to_string(), - object!({ - "type": "object", - "properties": {}, - "required": [] - }), - ) - .annotate(ToolAnnotations { - title: Some("Increment Tool".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }), - Tool::new( - "decrement".to_string(), - "Decrement the counter by 1".to_string(), - object!({ - "type": "object", - "properties": {}, - "required": [] - }), - ) - .annotate(ToolAnnotations { - title: Some("Decrement Tool".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }), - Tool::new( - "get_value".to_string(), - "Get the current counter value".to_string(), - object!({ - "type": "object", - "properties": {}, - "required": [] - }), - ) - .annotate(ToolAnnotations { - title: Some("Get Value Tool".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(false), - }), - ] - } - - fn call_tool( - &self, - tool_name: &str, - _arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - - Box::pin(async move { - match tool_name.as_str() { - "increment" => { - let value = this.increment().await?; - Ok(vec![Content::text(value.to_string())]) - } - "decrement" => { - let value = this.decrement().await?; - Ok(vec![Content::text(value.to_string())]) - } - "get_value" => { - let value = this.get_value().await?; - Ok(vec![Content::text(value.to_string())]) - } - _ => Err(ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(format!("Tool {} not found", tool_name)), - data: None, - }), - } - }) - } - - fn list_resources(&self) -> Vec { - vec![ - self._create_resource_text("str:////Users/to/some/path/", "cwd"), - self._create_resource_text("memo://insights", "memo-name"), - ] - } - - fn read_resource( - &self, - uri: &str, - ) -> Pin> + Send + 'static>> { - let uri = uri.to_string(); - Box::pin(async move { - match uri.as_str() { - "str:////Users/to/some/path/" => { - let cwd = "/Users/to/some/path/"; - Ok(cwd.to_string()) - } - "memo://insights" => { - let memo = - "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ..."; - Ok(memo.to_string()) - } - _ => Err(ResourceError::NotFound(format!( - "Resource {} not found", - uri - ))), - } - }) - } - - fn list_prompts(&self) -> Vec { - vec![Prompt::new( - "example_prompt", - Some("This is an example prompt that takes one required argument, message"), - Some(vec![PromptArgument { - name: "message".to_string(), - description: Some("A message to put in the prompt".to_string()), - required: Some(true), - }]), - )] - } - - fn get_prompt( - &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - match prompt_name.as_str() { - "example_prompt" => { - let prompt = "This is an example prompt with your message here: '{message}'"; - Ok(prompt.to_string()) - } - _ => Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))), - } - }) - } -} - -#[tokio::main] -async fn main() -> Result<()> { - // Set up file appender for logging - let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "mcp-server.log"); - - // Initialize the tracing subscriber with file and stdout logging - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) - .with_writer(file_appender) - .with_target(false) - .with_thread_ids(true) - .with_file(true) - .with_line_number(true) - .init(); - - tracing::info!("Starting MCP server"); - - // Create an instance of our counter router - let router = RouterService(CounterRouter::new()); - - // Create and run the server - let server = Server::new(router); - let transport = ByteTransport::new(stdin(), stdout()); - - tracing::info!("Server initialized and ready to handle requests"); - Ok(server.run(transport).await?) -} diff --git a/crates/mcp-server/src/router.rs b/crates/mcp-server/src/router.rs deleted file mode 100644 index 02dd318f7a01..000000000000 --- a/crates/mcp-server/src/router.rs +++ /dev/null @@ -1,425 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::{Context, Poll}, -}; - -type PromptFuture = Pin> + Send + 'static>>; -use mcp_core::{ - handler::{PromptError, ResourceError}, - protocol::{ - CallToolResult, Implementation, InitializeResult, ListPromptsResult, ListResourcesResult, - ListToolsResult, PromptsCapability, ReadResourceResult, ResourcesCapability, - ServerCapabilities, ToolsCapability, - }, -}; -use rmcp::model::{ - Content, ErrorData, GetPromptResult, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, - JsonRpcVersion2_0, Prompt, PromptMessage, PromptMessageRole, RequestId, Resource, - ResourceContents, -}; -use serde_json::Value; -use tokio::sync::mpsc; -use tower_service::Service; - -use crate::{BoxError, RouterError}; - -/// Builder for configuring and constructing capabilities -pub struct CapabilitiesBuilder { - tools: Option, - prompts: Option, - resources: Option, -} - -impl Default for CapabilitiesBuilder { - fn default() -> Self { - Self::new() - } -} - -impl CapabilitiesBuilder { - pub fn new() -> Self { - Self { - tools: None, - prompts: None, - resources: None, - } - } - - /// Add multiple tools to the router - pub fn with_tools(mut self, list_changed: bool) -> Self { - self.tools = Some(ToolsCapability { - list_changed: Some(list_changed), - }); - self - } - - /// Enable prompts capability - pub fn with_prompts(mut self, list_changed: bool) -> Self { - self.prompts = Some(PromptsCapability { - list_changed: Some(list_changed), - }); - self - } - - /// Enable resources capability - pub fn with_resources(mut self, subscribe: bool, list_changed: bool) -> Self { - self.resources = Some(ResourcesCapability { - subscribe: Some(subscribe), - list_changed: Some(list_changed), - }); - self - } - - /// Build the router with automatic capability inference - pub fn build(self) -> ServerCapabilities { - // Create capabilities based on what's configured - ServerCapabilities { - tools: self.tools, - prompts: self.prompts, - resources: self.resources, - } - } -} - -pub trait Router: Send + Sync + 'static { - fn name(&self) -> String; - // in the protocol, instructions are optional but we make it required - fn instructions(&self) -> String; - fn capabilities(&self) -> ServerCapabilities; - fn list_tools(&self) -> Vec; - fn call_tool( - &self, - tool_name: &str, - arguments: Value, - notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>>; - fn list_resources(&self) -> Vec; - fn read_resource( - &self, - uri: &str, - ) -> Pin> + Send + 'static>>; - fn list_prompts(&self) -> Vec; - fn get_prompt(&self, prompt_name: &str) -> PromptFuture; - - // Helper method to create base response - fn create_response(&self, id: RequestId) -> JsonRpcResponse { - JsonRpcResponse { - jsonrpc: JsonRpcVersion2_0, - id, - result: serde_json::Map::new(), - } - } - - // Helper method to set result on response - fn set_result( - &self, - response: &mut JsonRpcResponse, - result: T, - ) -> Result<(), RouterError> { - let value = serde_json::to_value(result) - .map_err(|e| RouterError::Internal(format!("JSON serialization error: {}", e)))?; - - if let Some(obj) = value.as_object() { - response.result = obj.clone(); - } else { - return Err(RouterError::Internal("Result must be a JSON object".into())); - } - - Ok(()) - } - - fn handle_initialize( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - let result = InitializeResult { - protocol_version: "2025-03-26".to_string(), - capabilities: self.capabilities().clone(), - server_info: Implementation { - name: self.name(), - version: env!("CARGO_PKG_VERSION").to_string(), - }, - instructions: Some(self.instructions()), - }; - - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_tools_list( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - let tools = self.list_tools(); - - let result = ListToolsResult { - tools, - next_cursor: None, - }; - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_tools_call( - &self, - req: JsonRpcRequest, - notifier: mpsc::Sender, - ) -> impl Future> + Send { - async move { - let params = &req.request.params; - - let name = params - .get("name") - .and_then(Value::as_str) - .ok_or_else(|| RouterError::InvalidParams("Missing tool name".into()))?; - - let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); - - let result = match self.call_tool(name, arguments, notifier).await { - Ok(result) => CallToolResult { - content: result, - is_error: None, - }, - Err(err) => CallToolResult { - content: vec![Content::text(err.to_string())], - is_error: Some(true), - }, - }; - - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_resources_list( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - let resources = self.list_resources(); - - let result = ListResourcesResult { - resources, - next_cursor: None, - }; - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_resources_read( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - let params = &req.request.params; - - let uri = params - .get("uri") - .and_then(Value::as_str) - .ok_or_else(|| RouterError::InvalidParams("Missing resource URI".into()))?; - - let contents = self.read_resource(uri).await.map_err(RouterError::from)?; - - let result = ReadResourceResult { - contents: vec![ResourceContents::TextResourceContents { - uri: uri.to_string(), - mime_type: Some("text/plain".to_string()), - text: contents, - meta: None, - }], - }; - - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_prompts_list( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - let prompts = self.list_prompts(); - - let result = ListPromptsResult { prompts }; - - let mut response = self.create_response(req.id); - self.set_result(&mut response, result)?; - Ok(response) - } - } - - fn handle_prompts_get( - &self, - req: JsonRpcRequest, - ) -> impl Future> + Send { - async move { - // Validate and extract parameters - let params = &req.request.params; - - // Extract "name" field - let prompt_name = params - .get("name") - .and_then(Value::as_str) - .ok_or_else(|| RouterError::InvalidParams("Missing prompt name".into()))?; - - // Extract "arguments" field - let arguments = params - .get("arguments") - .and_then(Value::as_object) - .ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?; - - // Fetch the prompt definition first - let prompt = self - .list_prompts() - .into_iter() - .find(|p| p.name == prompt_name) - .ok_or_else(|| { - RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name)) - })?; - - // Validate required arguments - if let Some(args) = &prompt.arguments { - for arg in args { - if arg.required.is_some() - && arg.required.unwrap() - && (!arguments.contains_key(&arg.name) - || arguments - .get(&arg.name) - .and_then(Value::as_str) - .is_none_or(str::is_empty)) - { - return Err(RouterError::InvalidParams(format!( - "Missing required argument: '{}'", - arg.name - ))); - } - } - } - - // Now get the prompt content - let description = self - .get_prompt(prompt_name) - .await - .map_err(|e| RouterError::Internal(e.to_string()))?; - - // Validate prompt arguments for potential security issues from user text input - // Checks: - // - Prompt must be less than 10000 total characters - // - Argument keys must be less than 1000 characters - // - Argument values must be less than 1000 characters - // - Dangerous patterns, eg "../", "//", "\\\\", "