diff --git a/Cargo.lock b/Cargo.lock index 07c8360..a177319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -93,22 +87,15 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bee399cc3a623ec5a2db2c5b90ee0190a2260241fbe0c023ac8f7bab426aaf8" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" dependencies = [ - "bzip2", "compression-codecs", "compression-core", - "deflate64", - "flate2", "futures-core", "futures-io", - "liblzma", - "memchr", "pin-project-lite", - "zstd", - "zstd-safe", ] [[package]] @@ -159,11 +146,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" @@ -199,10 +192,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.34" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -216,23 +210,22 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "clap" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -240,9 +233,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -252,9 +245,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -276,18 +269,15 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compression-codecs" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7eea68f0e02c2b0aa8856e9a9478444206d4b6828728e7b0697c0f8cca265cb" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" dependencies = [ "bzip2", "compression-core", "deflate64", "flate2", - "futures-core", "liblzma", - "memchr", - "pin-project-lite", "zstd", "zstd-safe", ] @@ -319,27 +309,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "dirs" version = "6.0.0" @@ -358,7 +327,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -381,12 +350,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -395,6 +364,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "flate2" version = "1.1.2" @@ -527,7 +502,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -642,9 +617,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -699,9 +674,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -741,9 +716,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -751,9 +726,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" @@ -767,9 +742,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" @@ -978,7 +953,6 @@ dependencies = [ "async_zip", "chrono", "clap", - "derive_more", "dirs", "futures", "glob", @@ -996,9 +970,9 @@ dependencies = [ [[package]] name = "rust-mcp-macros" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2079014c2b3dfa82a2dafd203121d5a28929102c5e003bf8d8019a60fc17e0d" +checksum = "b647a85c9da2eaf14e67d39cb067a8157a66bd2c0dc53ef1051a84f45edfae24" dependencies = [ "proc-macro2", "quote", @@ -1009,9 +983,9 @@ dependencies = [ [[package]] name = "rust-mcp-schema" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098436b06bfa4b88b110d12a5567cf37fd454735ee67cab7eb48bdbea0dd0e57" +checksum = "0bb65fd293dbbfabaacba1512b3948cdd9bf31ad1f2c0fed4962052b590c5c44" dependencies = [ "serde", "serde_json", @@ -1019,11 +993,12 @@ dependencies = [ [[package]] name = "rust-mcp-sdk" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720fb8f08c2b4525cf84c923a1199f326d58f82e37d9cf4bb751b9411b023cec" +checksum = "961ec01d0bedecf488388e6b1cf04170f9badab4927061c6592ffa385c02c6c9" dependencies = [ "async-trait", + "base64", "futures", "rust-mcp-macros", "rust-mcp-schema", @@ -1033,13 +1008,14 @@ dependencies = [ "thiserror", "tokio", "tracing", + "uuid", ] [[package]] name = "rust-mcp-transport" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc874c3f07ad7aff5e5d8eab59174377b5ac0dbe2c0ef70d09efef9cdf4649b" +checksum = "35feabc5e4667019dc262178724c94cbced6f43959af15e214b52f79243f55ed" dependencies = [ "async-trait", "bytes", @@ -1061,15 +1037,15 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1101,18 +1077,28 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -1121,14 +1107,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1193,15 +1180,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1322,15 +1309,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "utf8parse" @@ -1338,6 +1319,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1356,30 +1348,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -1391,9 +1393,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1401,9 +1403,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -1414,31 +1416,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -1471,22 +1473,28 @@ 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-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -1507,6 +1515,15 @@ 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.52.6" @@ -1529,7 +1546,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", @@ -1638,9 +1655,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "zstd" @@ -1662,9 +1679,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 0d62d72..40043de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rust-mcp-filesystem" version = "0.2.3" -edition = "2021" +edition = "2024" repository = "https://github.com/rust-mcp-stack/rust-mcp-filesystem" authors = ["Ali Hashemi"] description = "Blazing-fast, asynchronous MCP server for seamless filesystem operations." @@ -15,9 +15,10 @@ license = false eula = false [dependencies] -rust-mcp-sdk = { version = "0.6", default-features = false, features = [ +rust-mcp-sdk = {version="0.7", default-features = false, features = [ "server", "macros", + "stdio", "2025_06_18", ] } @@ -25,7 +26,6 @@ thiserror = { version = "2.0" } dirs = "6.0" glob = "0.3" walkdir = "2.5" -derive_more = { version = "2.0", features = ["display", "from_str"] } similar = "=2.7" chrono = "0.4" clap = { version = "4.5", features = ["derive"] } diff --git a/README.md b/README.md index 4f0bf8f..e1925b3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Rust MCP Filesystem -Rust MCP Filesystem is a blazingly fast, asynchronous, and lightweight MCP (Model Context Protocol) server designed for efficient handling of various filesystem operations. +Rust MCP Filesystem is a blazingly fast, asynchronous, and lightweight MCP (Model Context Protocol) server designed for efficient handling of various filesystem operations. This project is a pure Rust rewrite of the JavaScript-based `@modelcontextprotocol/server-filesystem`, offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. 🚀 Refer to the [project documentation](https://rust-mcp-stack.github.io/rust-mcp-filesystem) for installation and configuration instructions. @@ -14,8 +14,9 @@ This project is a pure Rust rewrite of the JavaScript-based `@modelcontextprotoc - **⚡ High Performance**: Built in Rust for speed and efficiency, leveraging asynchronous I/O to handle filesystem operations seamlessly. - **🔒 Read-Only by Default**: Starts with no write access, ensuring safety until explicitly configured otherwise. - **🔍 Advanced Glob Search**: Supports full glob pattern matching allowing precise filtering of files and directories using standard glob syntax.For example, patterns like `*.rs`, `src/**/*.txt`, and `logs/error-???.log` are valid and can be used to match specific file types, recursive directory searches, or patterned filenames. -- **📁 Nested Directories**: Improved directory creation, allowing the creation of nested directories. -- **ðŸ“Ķ Lightweight**: Standalone with no external dependencies (e.g., no Node.js, Python etc required), compiled to a single binary with a minimal resource footprint, ideal for both lightweight and extensive deployment scenarios. +- **🔄 MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default). +- **ðŸ“Ķ ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease. +- **ðŸŠķ Lightweight**: Standalone with no external dependencies (e.g., no Node.js, Python etc required), compiled to a single binary with a minimal resource footprint, ideal for both lightweight and extensive deployment scenarios. #### 👉 Refer to [capabilities](https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities) for a full list of tools and other capabilities. diff --git a/docs/README.md b/docs/README.md index 3ea38c8..b340427 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Rust MCP Filesystem -Rust MCP Filesystem is a blazingly fast, asynchronous, and lightweight MCP (Model Context Protocol) server designed for efficient handling of various filesystem operations. +Rust MCP Filesystem is a blazingly fast, asynchronous, and lightweight MCP (Model Context Protocol) server designed for efficient handling of various filesystem operations. This project is a pure Rust rewrite of the JavaScript-based **@modelcontextprotocol/server-filesystem**, offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. Refer to the [quickstart](quickstart.md) guide for installation and configuration instructions. @@ -9,7 +9,8 @@ Refer to the [quickstart](quickstart.md) guide for installation and configuratio - **⚡ High Performance**: Built in Rust for speed and efficiency, leveraging asynchronous I/O to handle filesystem operations seamlessly. - **🔒 Read-Only by Default**: Starts with no write access, ensuring safety until explicitly configured otherwise. -- **🔍 Advanced Glob Search**: Full glob pattern matching for precise file and directory filtering (e.g., `*.rs`, `src/**/*.txt`, `logs/error-???.log`). +- **🔍 Advanced Glob Search**: Supports full glob pattern matching allowing precise filtering of files and directories using standard glob syntax.For example, patterns like `*.rs`, `src/**/*.txt`, and `logs/error-???.log` are valid and can be used to match specific file types, recursive directory searches, or patterned filenames. +- **🔄 MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default). - **ðŸ“Ķ ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease. - **ðŸŠķ Lightweight**: Standalone with no external dependencies (e.g., no Node.js, Python etc required), compiled to a single binary with a minimal resource footprint, ideal for both lightweight and extensive deployment scenarios. diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 048af70..f2e15a3 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -14,7 +14,8 @@ - ðŸŠķ Lightweight - ⚡ High Performance -- 🔒 Read-Only by Default +- 🔒 Secure by design +- 🔧 Packed with powerful tools [GitHub](https://github.com/rust-mcp-stack/rust-mcp-filesystem) [⚙ïļ Capabilities](capabilities.md) diff --git a/docs/guide/cli-command-options.md b/docs/guide/cli-command-options.md index 0a430a7..894483c 100644 --- a/docs/guide/cli-command-options.md +++ b/docs/guide/cli-command-options.md @@ -1,10 +1,10 @@ ## CLI Command Options ```sh -Usage: rust-mcp-filesystem [OPTIONS] ... +Usage: rust-mcp-filesystem [OPTIONS] [ALLOWED_DIRECTORIES]... Arguments: - ... + [ALLOWED_DIRECTORIES]... Provide a space-separated list of directories that are permitted for the operation. This list allows multiple directories to be provided. @@ -12,7 +12,12 @@ Arguments: Options: -w, --allow-write - Enables read/write mode for the app, allowing both reading and writing. + Enables read/write mode for the app, allowing both reading and writing. Defaults to disabled. + + -t, --enable-roots + Enables dynamic directory access control via Roots from the MCP client side. Defaults to disabled. + When enabled, MCP clients that support Roots can dynamically update the allowed directories. + Any directories provided by the client will completely replace the initially configured allowed directories on the server. -h, --help Print help (see a summary with '-h') diff --git a/src/cli.rs b/src/cli.rs index df09c3b..61a3644 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,21 +1,40 @@ -use clap::{arg, command, Parser}; +use clap::{Parser, arg, command}; #[derive(Parser, Debug)] #[command(name = env!("CARGO_PKG_NAME"))] #[command(version = env!("CARGO_PKG_VERSION"))] -#[command(about = "A lightning-fast, asynchronous, and lightweight MCP server designed for efficient handling of various filesystem operations", +#[command(about = "A lightning-fast, asynchronous, and lightweight MCP server designed for efficient handling of various filesystem operations", long_about = None)] pub struct CommandArguments { #[arg( short = 'w', long, - help = "Enables read/write mode for the app, allowing both reading and writing." + help = "Enables read/write mode for the app, allowing both reading and writing. Defaults to disabled." )] pub allow_write: bool, #[arg( - help = "List of directories that are permitted for the operation.", + help = "List of directories that are permitted for the operation. It is required when 'enable-roots' is not provided OR client does not support Roots.", long_help = concat!("Provide a space-separated list of directories that are permitted for the operation.\nThis list allows multiple directories to be provided.\n\nExample: ", env!("CARGO_PKG_NAME"), " /path/to/dir1 /path/to/dir2 /path/to/dir3"), - required = true + required = false )] pub allowed_directories: Vec, + + #[arg( + short = 't', + long, + help = "Enables dynamic directory access control via Roots from the MCP client side. Defaults to disabled.\nWhen enabled, MCP clients that support Roots can dynamically update the allowed directories.\nAny directories provided by the client will completely replace the initially configured allowed directories on the server." + )] + pub enable_roots: bool, +} + +impl CommandArguments { + pub fn validate(&self) -> Result<(), String> { + if !self.enable_roots && self.allowed_directories.is_empty() { + return Err(format!( + " is required when `--enable-roots` is not provided.\n Run `{} --help` to view the usage instructions.", + env!("CARGO_PKG_NAME") + )); + } + Ok(()) + } } diff --git a/src/error.rs b/src/error.rs index 07ee74e..2675360 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ use async_zip::error::ZipError; use glob::PatternError; -use rust_mcp_sdk::schema::{schema_utils::SdkError, RpcError}; -use rust_mcp_sdk::{error::McpSdkError, TransportError}; +use rust_mcp_sdk::schema::{RpcError, schema_utils::SdkError}; +use rust_mcp_sdk::{TransportError, error::McpSdkError}; use thiserror::Error; use tokio::io; @@ -10,7 +10,9 @@ pub type ServiceResult = core::result::Result; #[derive(Debug, Error)] pub enum ServiceError { - #[error("Service is running in read-only mode. To enable write access, please run with the --allow-write flag.")] + #[error( + "Service is running in read-only mode. To enable write access, please run with the --allow-write flag." + )] NoWriteAccess, #[error("{0}")] FromString(String), diff --git a/src/fs_service.rs b/src/fs_service.rs index 32bfcb0..ef32c94 100644 --- a/src/fs_service.rs +++ b/src/fs_service.rs @@ -1,26 +1,31 @@ pub mod file_info; pub mod utils; +use crate::{ + error::{ServiceError, ServiceResult}, + tools::EditOperation, +}; +use async_zip::tokio::{read::seek::ZipFileReader, write::ZipFileWriter}; use file_info::FileInfo; +use glob::Pattern; use grep::{ matcher::{Match, Matcher}, regex::RegexMatcherBuilder, - searcher::{sinks::UTF8, BinaryDetection, Searcher}, + searcher::{BinaryDetection, Searcher, sinks::UTF8}, }; -use serde_json::{json, Value}; - +use rust_mcp_sdk::schema::RpcError; +use serde_json::{Value, json}; +use similar::TextDiff; use std::{ + collections::HashSet, env, fs::{self}, path::{Path, PathBuf}, + sync::Arc, }; - -use async_zip::tokio::{read::seek::ZipFileReader, write::ZipFileWriter}; -use glob::Pattern; -use rust_mcp_sdk::schema::RpcError; -use similar::TextDiff; use tokio::{ fs::File, io::{AsyncWriteExt, BufReader}, + sync::RwLock, }; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use utils::{ @@ -29,16 +34,13 @@ use utils::{ }; use walkdir::WalkDir; -use crate::{ - error::{ServiceError, ServiceResult}, - tools::EditOperation, -}; - const SNIPPET_MAX_LENGTH: usize = 200; const SNIPPET_BACKWARD_CHARS: usize = 30; +type PathResultList = Vec>; + pub struct FileSystemService { - allowed_path: Vec, + allowed_path: RwLock>>, } /// Represents a single match found in a file's content. @@ -75,17 +77,72 @@ impl FileSystemService { .collect(); Ok(Self { - allowed_path: normalized_dirs, + allowed_path: RwLock::new(Arc::new(normalized_dirs)), }) } - pub fn allowed_directories(&self) -> &Vec { - &self.allowed_path + pub async fn allowed_directories(&self) -> Arc> { + let guard = self.allowed_path.read().await; + guard.clone() } } impl FileSystemService { - pub fn validate_path(&self, requested_path: &Path) -> ServiceResult { + pub fn valid_roots(&self, roots: Vec<&str>) -> ServiceResult<(Vec, Option)> { + let paths: Vec> = roots + .iter() + .map(|p| self.parse_file_path(p)) + .collect::>(); + + // Partition into Ok and Err results + let (ok_paths, err_paths): (PathResultList, PathResultList) = + paths.into_iter().partition(|p| p.is_ok()); + + // using HashSet to remove duplicates + let (valid_roots, no_dir_roots): (HashSet, HashSet) = ok_paths + .into_iter() + .collect::, _>>()? + .into_iter() + .map(expand_home) + .partition(|path| path.is_dir()); + + let skipped_roots = if !err_paths.is_empty() || !no_dir_roots.is_empty() { + Some(format!( + "Warning: skipped {} invalid roots.", + err_paths.len() + no_dir_roots.len() + )) + } else { + None + }; + + let valid_roots = valid_roots.into_iter().collect(); + + Ok((valid_roots, skipped_roots)) + } + + pub async fn update_allowed_paths(&self, valid_roots: Vec) { + let mut guard = self.allowed_path.write().await; + *guard = Arc::new(valid_roots) + } + + /// Converts a string to a `PathBuf`, supporting both raw paths and `file://` URIs. + fn parse_file_path(&self, input: &str) -> ServiceResult { + Ok(PathBuf::from( + input.strip_prefix("file://").unwrap_or(input).trim(), + )) + } + + pub fn validate_path( + &self, + requested_path: &Path, + allowed_directories: Arc>, + ) -> ServiceResult { + if allowed_directories.is_empty() { + return Err(ServiceError::FromString( + "Allowed directories list is empty. Client did not provide any valid root directories.".to_string() + )); + } + // Expand ~ to home directory let expanded_path = expand_home(requested_path.to_path_buf()); @@ -100,7 +157,7 @@ impl FileSystemService { let normalized_requested = normalize_path(&absolute_path); // Check if path is within allowed directories - if !self.allowed_path.iter().any(|dir| { + if !allowed_directories.iter().any(|dir| { // Must account for both scenarios — the requested path may not exist yet, making canonicalization impossible. normalized_requested.starts_with(dir) || normalized_requested.starts_with(normalize_path(dir)) @@ -114,7 +171,7 @@ impl FileSystemService { "Access denied - {} is outside allowed directories: {} not in {}", symlink_target, absolute_path.display(), - self.allowed_path + allowed_directories .iter() .map(|p| p.display().to_string()) .collect::>() @@ -127,7 +184,8 @@ impl FileSystemService { // Get file stats pub async fn get_file_stats(&self, file_path: &Path) -> ServiceResult { - let valid_path = self.validate_path(file_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(file_path, allowed_directories)?; let metadata = fs::metadata(valid_path)?; @@ -165,7 +223,9 @@ impl FileSystemService { pattern: String, target_zip_file: String, ) -> ServiceResult { - let valid_dir_path = self.validate_path(Path::new(&input_dir))?; + let allowed_directories = self.allowed_directories().await; + let valid_dir_path = + self.validate_path(Path::new(&input_dir), allowed_directories.clone())?; let input_dir_str = &valid_dir_path .as_os_str() @@ -175,7 +235,8 @@ impl FileSystemService { "Invalid UTF-8 in file name", ))?; - let target_path = self.validate_path(Path::new(&target_zip_file))?; + let target_path = + self.validate_path(Path::new(&target_zip_file), allowed_directories.clone())?; if target_path.exists() { return Err(std::io::Error::new( @@ -200,13 +261,17 @@ impl FileSystemService { .filter_map(|entry| { let full_path = entry.path(); - self.validate_path(full_path).ok().and_then(|path| { - if path != valid_dir_path && glob_pattern.matches(&path.display().to_string()) { - Some(path) - } else { - None - } - }) + self.validate_path(full_path, allowed_directories.clone()) + .ok() + .and_then(|path| { + if path != valid_dir_path + && glob_pattern.matches(&path.display().to_string()) + { + Some(path) + } else { + None + } + }) }) .collect(); @@ -264,8 +329,9 @@ impl FileSystemService { ) .into()); } - - let target_path = self.validate_path(Path::new(&target_zip_file))?; + let allowed_directories = self.allowed_directories().await; + let target_path = + self.validate_path(Path::new(&target_zip_file), allowed_directories.clone())?; if target_path.exists() { return Err(std::io::Error::new( @@ -277,7 +343,7 @@ impl FileSystemService { let source_paths = input_files .iter() - .map(|p| self.validate_path(Path::new(p))) + .map(|p| self.validate_path(Path::new(p), allowed_directories.clone())) .collect::, _>>()?; let zip_file = File::create(&target_path).await?; @@ -314,8 +380,10 @@ impl FileSystemService { } pub async fn unzip_file(&self, zip_file: &str, target_dir: &str) -> ServiceResult { - let zip_file = self.validate_path(Path::new(&zip_file))?; - let target_dir_path = self.validate_path(Path::new(target_dir))?; + let allowed_directories = self.allowed_directories().await; + + let zip_file = self.validate_path(Path::new(&zip_file), allowed_directories.clone())?; + let target_dir_path = self.validate_path(Path::new(target_dir), allowed_directories)?; if !zip_file.exists() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, @@ -365,26 +433,31 @@ impl FileSystemService { } pub async fn read_file(&self, file_path: &Path) -> ServiceResult { - let valid_path = self.validate_path(file_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(file_path, allowed_directories)?; let content = tokio::fs::read_to_string(valid_path).await?; Ok(content) } pub async fn create_directory(&self, file_path: &Path) -> ServiceResult<()> { - let valid_path = self.validate_path(file_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(file_path, allowed_directories)?; tokio::fs::create_dir_all(valid_path).await?; Ok(()) } pub async fn move_file(&self, src_path: &Path, dest_path: &Path) -> ServiceResult<()> { - let valid_src_path = self.validate_path(src_path)?; - let valid_dest_path = self.validate_path(dest_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_src_path = self.validate_path(src_path, allowed_directories.clone())?; + let valid_dest_path = self.validate_path(dest_path, allowed_directories)?; tokio::fs::rename(valid_src_path, valid_dest_path).await?; Ok(()) } pub async fn list_directory(&self, dir_path: &Path) -> ServiceResult> { - let valid_path = self.validate_path(dir_path)?; + let allowed_directories = self.allowed_directories().await; + + let valid_path = self.validate_path(dir_path, allowed_directories)?; let mut dir = tokio::fs::read_dir(valid_path).await?; @@ -399,7 +472,8 @@ impl FileSystemService { } pub async fn write_file(&self, file_path: &Path, content: &String) -> ServiceResult<()> { - let valid_path = self.validate_path(file_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(file_path, allowed_directories)?; tokio::fs::write(valid_path, content).await?; Ok(()) } @@ -416,13 +490,15 @@ impl FileSystemService { /// # Returns /// A `ServiceResult` containing a vector of`walkdir::DirEntry` objects for matching files, /// or a `ServiceError` if an error occurs. - pub fn search_files( + pub async fn search_files( &self, root_path: &Path, pattern: String, exclude_patterns: Vec, ) -> ServiceResult> { - let result = self.search_files_iter(root_path, pattern, exclude_patterns)?; + let result = self + .search_files_iter(root_path, pattern, exclude_patterns) + .await?; Ok(result.collect::>()) } @@ -437,14 +513,15 @@ impl FileSystemService { /// # Returns /// A `ServiceResult` containing an iterator yielding `walkdir::DirEntry` objects for matching files, /// or a `ServiceError` if an error occurs. - pub fn search_files_iter<'a>( + pub async fn search_files_iter<'a>( &'a self, // root_path: impl Into, root_path: &'a Path, pattern: String, exclude_patterns: Vec, ) -> ServiceResult + 'a> { - let valid_path = self.validate_path(root_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(root_path, allowed_directories.clone())?; let updated_pattern = if pattern.contains('*') { pattern.to_lowercase() @@ -460,7 +537,9 @@ impl FileSystemService { let full_path = dir_entry.path(); // Validate each path before processing - let validated_path = self.validate_path(full_path).ok(); + let validated_path = self + .validate_path(full_path, allowed_directories.clone()) + .ok(); if validated_path.is_none() { // Skip invalid paths during search @@ -489,14 +568,12 @@ impl FileSystemService { if root_path == entry.path() { return false; } - - let is_match = glob_pattern + glob_pattern .as_ref() .map(|glob| { glob.matches(&entry.file_name().to_str().unwrap_or("").to_lowercase()) }) - .unwrap_or(false); - is_match + .unwrap_or(false) }); Ok(result) @@ -522,8 +599,9 @@ impl FileSystemService { max_depth: Option, max_files: Option, current_count: &mut usize, + allowed_directories: Arc>, ) -> ServiceResult<(Value, bool)> { - let valid_path = self.validate_path(root_path.as_ref())?; + let valid_path = self.validate_path(root_path.as_ref(), allowed_directories.clone())?; let metadata = fs::metadata(&valid_path)?; if !metadata.is_dir() { @@ -569,8 +647,13 @@ impl FileSystemService { if metadata.is_dir() { let next_depth = max_depth.map(|d| d - 1); - let (child_children, child_reached_max_depth) = - self.directory_tree(child_path, next_depth, max_files, current_count)?; + let (child_children, child_reached_max_depth) = self.directory_tree( + child_path, + next_depth, + max_files, + current_count, + allowed_directories.clone(), + )?; json_entry .as_object_mut() .unwrap() @@ -620,7 +703,8 @@ impl FileSystemService { dry_run: Option, save_to: Option<&Path>, ) -> ServiceResult { - let valid_path = self.validate_path(file_path)?; + let allowed_directories = self.allowed_directories().await; + let valid_path = self.validate_path(file_path, allowed_directories)?; // Read file content and normalize line endings let content_str = tokio::fs::read_to_string(&valid_path).await?; @@ -923,7 +1007,7 @@ impl FileSystemService { result } - pub fn search_files_content( + pub async fn search_files_content( &self, root_path: impl AsRef, pattern: &str, @@ -931,11 +1015,13 @@ impl FileSystemService { is_regex: bool, exclude_patterns: Option>, ) -> ServiceResult> { - let files_iter = self.search_files_iter( - root_path.as_ref(), - pattern.to_string(), - exclude_patterns.to_owned().unwrap_or_default(), - )?; + let files_iter = self + .search_files_iter( + root_path.as_ref(), + pattern.to_string(), + exclude_patterns.to_owned().unwrap_or_default(), + ) + .await?; let results: Vec = files_iter .filter_map(|entry| { diff --git a/src/fs_service/utils.rs b/src/fs_service/utils.rs index 189c69d..05c7ae2 100644 --- a/src/fs_service/utils.rs +++ b/src/fs_service/utils.rs @@ -4,7 +4,7 @@ use std::{ time::SystemTime, }; -use async_zip::{error::ZipError, tokio::write::ZipFileWriter, Compression, ZipEntryBuilder}; +use async_zip::{Compression, ZipEntryBuilder, error::ZipError, tokio::write::ZipFileWriter}; use chrono::{DateTime, Local}; use dirs::home_dir; diff --git a/src/handler.rs b/src/handler.rs index da3ef8a..822e81e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,27 +1,31 @@ use std::cmp::Ordering; +use std::sync::Arc; use crate::cli::CommandArguments; use crate::error::ServiceError; use crate::{error::ServiceResult, fs_service::FileSystemService, tools::*}; use async_trait::async_trait; +use rust_mcp_sdk::McpServer; use rust_mcp_sdk::mcp_server::ServerHandler; +use rust_mcp_sdk::schema::RootsListChangedNotification; use rust_mcp_sdk::schema::{ - schema_utils::CallToolError, CallToolRequest, CallToolResult, InitializeRequest, - InitializeResult, ListToolsRequest, ListToolsResult, RpcError, + CallToolRequest, CallToolResult, InitializeRequest, InitializeResult, ListToolsRequest, + ListToolsResult, RpcError, schema_utils::CallToolError, }; -use rust_mcp_sdk::McpServer; -pub struct MyServerHandler { +pub struct FileSystemHandler { readonly: bool, - fs_service: FileSystemService, + mcp_roots_support: bool, + fs_service: Arc, } -impl MyServerHandler { +impl FileSystemHandler { pub fn new(args: &CommandArguments) -> ServiceResult { let fs_service = FileSystemService::try_new(&args.allowed_directories)?; Ok(Self { - fs_service, - readonly: !&args.allow_write, + fs_service: Arc::new(fs_service), + readonly: !args.allow_write, + mcp_roots_support: args.enable_roots, }) } @@ -33,35 +37,121 @@ impl MyServerHandler { } } - pub fn startup_message(&self) -> String { - format!( - "Secure MCP Filesystem Server running in \"{}\" mode.\nAllowed directories:\n{}", + pub async fn startup_message(&self) -> String { + let common_message = format!( + "Secure MCP Filesystem Server running in \"{}\" mode {} \"MCP Roots\" support.", if !self.readonly { "read/write" } else { "readonly" }, - self.fs_service - .allowed_directories() - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(",\n") - ) + if self.mcp_roots_support { + "with" + } else { + "without" + }, + ); + + let allowed_directories = self.fs_service.allowed_directories().await; + let sub_message: String = if allowed_directories.is_empty() && self.mcp_roots_support { + "No allowed directories is set - waiting for client to provide roots via MCP protocol...".to_string() + } else { + format!( + "Allowed directories:\n{}", + allowed_directories + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(",\n") + ) + }; + + format!("{common_message}\n{sub_message}") + } + + pub(crate) async fn update_allowed_directories(&self, runtime: Arc) { + // if client does not support roots + let allowed_directories = self.fs_service.allowed_directories().await; + if !runtime.client_supports_root_list().unwrap_or(false) { + if !allowed_directories.is_empty() { + let _ = runtime.stderr_message(format!("Client does not support MCP Roots, using allowed directories set from server args:\n{}", allowed_directories + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(",\n"))).await; + } else { + // let message = "Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories."; + let message = "Server cannot operate: No allowed directories available. Server was started without command-line directories and client does not support MCP roots protocol. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories."; + let _ = runtime.stderr_message(message.to_string()).await; + } + } else { + let fs_service = self.fs_service.clone(); + let mcp_roots_support = self.mcp_roots_support; + // retrieve roots from the client and update the allowed directories accordingly + tokio::spawn(async move { + let roots = match runtime.clone().list_roots(None).await { + Ok(roots_result) => roots_result.roots, + Err(_err) => { + vec![] + } + }; + + let valid_roots = if roots.is_empty() { + vec![] + } else { + let roots: Vec<_> = roots.iter().map(|v| v.uri.as_str()).collect(); + + match fs_service.valid_roots(roots) { + Ok((roots, skipped)) => { + if let Some(message) = skipped { + let _ = runtime.stderr_message(message.to_string()).await; + } + roots + } + Err(_err) => vec![], + } + }; + + if valid_roots.is_empty() && !mcp_roots_support { + let message = if allowed_directories.is_empty() { + "Server cannot operate: No allowed directories available. Server was started without command-line directories and client provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories." + } else { + "Client provided empty roots. Allowed directories passed from command-line will be used." + }; + let _ = runtime.stderr_message(message.to_string()).await; + } else { + let num_valid_roots = valid_roots.len(); + + fs_service.update_allowed_paths(valid_roots).await; + let message = format!( + "Updated allowed directories from MCP roots: {num_valid_roots} valid directories", + ); + let _ = runtime.stderr_message(message.to_string()).await; + } + }); + } } } #[async_trait] -impl ServerHandler for MyServerHandler { - async fn on_server_started(&self, runtime: &dyn McpServer) { - let _ = runtime.stderr_message(self.startup_message()).await; +impl ServerHandler for FileSystemHandler { + async fn on_initialized(&self, runtime: Arc) { + let _ = runtime.stderr_message(self.startup_message().await).await; + self.update_allowed_directories(runtime).await; } - async fn on_initialized(&self, _: &dyn McpServer) {} + async fn handle_roots_list_changed_notification( + &self, + _notification: RootsListChangedNotification, + runtime: Arc, + ) -> std::result::Result<(), RpcError> { + self.update_allowed_directories(runtime).await; + Ok(()) + } async fn handle_list_tools_request( &self, _: ListToolsRequest, - _: &dyn McpServer, + _: Arc, ) -> std::result::Result { Ok(ListToolsResult { tools: FileSystemTools::tools(), @@ -73,7 +163,7 @@ impl ServerHandler for MyServerHandler { async fn handle_initialize_request( &self, initialize_request: InitializeRequest, - runtime: &dyn McpServer, + runtime: Arc, ) -> std::result::Result { runtime .set_client_details(initialize_request.params.clone()) @@ -95,7 +185,7 @@ impl ServerHandler for MyServerHandler { async fn handle_call_tool_request( &self, request: CallToolRequest, - _: &dyn McpServer, + _: Arc, ) -> std::result::Result { let tool_params: FileSystemTools = FileSystemTools::try_from(request.params).map_err(CallToolError::new)?; diff --git a/src/main.rs b/src/main.rs index fadeb83..ef0e08e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,16 @@ use clap::Parser; -use rust_mcp_filesystem::{cli, error::ServiceResult, server}; +use rust_mcp_filesystem::{cli, server}; #[tokio::main] -async fn main() -> ServiceResult<()> { - server::start_server(cli::CommandArguments::parse()).await +async fn main() { + let arguments = cli::CommandArguments::parse(); + if let Err(err) = arguments.validate() { + eprintln!("Error: {err}"); + return; + }; + + if let Err(error) = server::start_server(arguments).await { + eprintln!("{error}"); + } + println!(">>> 90 {:?} ", 90); } diff --git a/src/server.rs b/src/server.rs index af7151e..133e664 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,11 @@ use rust_mcp_sdk::schema::{ - Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, - LATEST_PROTOCOL_VERSION, + Implementation, InitializeResult, LATEST_PROTOCOL_VERSION, ServerCapabilities, + ServerCapabilitiesTools, }; -use rust_mcp_sdk::{mcp_server::server_runtime, McpServer, StdioTransport, TransportOptions}; +use rust_mcp_sdk::{McpServer, StdioTransport, TransportOptions, mcp_server::server_runtime}; -use crate::{cli::CommandArguments, error::ServiceResult, handler::MyServerHandler}; +use crate::handler::FileSystemHandler; +use crate::{cli::CommandArguments, error::ServiceResult}; pub fn server_details() -> InitializeResult { InitializeResult { @@ -30,7 +31,7 @@ pub fn server_details() -> InitializeResult { pub async fn start_server(args: CommandArguments) -> ServiceResult<()> { let transport = StdioTransport::new(TransportOptions::default())?; - let handler = MyServerHandler::new(&args)?; + let handler = FileSystemHandler::new(&args)?; let server = server_runtime::create_server(server_details(), transport, handler); server.start().await?; diff --git a/src/tools/create_directory.rs b/src/tools/create_directory.rs index 48f01ff..36dcbbc 100644 --- a/src/tools/create_directory.rs +++ b/src/tools/create_directory.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/directory_tree.rs b/src/tools/directory_tree.rs index c6555b7..a347ea2 100644 --- a/src/tools/directory_tree.rs +++ b/src/tools/directory_tree.rs @@ -1,7 +1,7 @@ -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; -use serde_json::{json, Map, Value}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; +use serde_json::{Map, Value, json}; use crate::error::ServiceError; use crate::fs_service::FileSystemService; @@ -33,12 +33,16 @@ impl DirectoryTreeTool { context: &FileSystemService, ) -> std::result::Result { let mut entry_counter: usize = 0; + + let allowed_directories = context.allowed_directories().await; + let (entries, reached_max_depth) = context .directory_tree( params.path, params.max_depth.map(|v| v as usize), None, &mut entry_counter, + allowed_directories, ) .map_err(CallToolError::new)?; diff --git a/src/tools/edit_file.rs b/src/tools/edit_file.rs index a29311c..0d63fbe 100644 --- a/src/tools/edit_file.rs +++ b/src/tools/edit_file.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/get_file_info.rs b/src/tools/get_file_info.rs index 55c3a03..4d5d42b 100644 --- a/src/tools/get_file_info.rs +++ b/src/tools/get_file_info.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/list_allowed_directories.rs b/src/tools/list_allowed_directories.rs index 36e52c2..bf401ea 100644 --- a/src/tools/list_allowed_directories.rs +++ b/src/tools/list_allowed_directories.rs @@ -1,6 +1,6 @@ -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; @@ -24,15 +24,20 @@ impl ListAllowedDirectoriesTool { _: Self, context: &FileSystemService, ) -> std::result::Result { - let result = format!( - "Allowed directories:\n{}", - context - .allowed_directories() - .iter() - .map(|entry| entry.display().to_string()) - .collect::>() - .join("\n") - ); + let allowed_directories = context.allowed_directories().await; + + let result = if allowed_directories.is_empty() { + "Allowed directories list is empty!".to_string() + } else { + format!( + "Allowed directories:\n{}", + allowed_directories + .iter() + .map(|entry| entry.display().to_string()) + .collect::>() + .join("\n") + ) + }; Ok(CallToolResult::text_content(vec![TextContent::from( result, )])) diff --git a/src/tools/list_directory.rs b/src/tools/list_directory.rs index cb0b95e..2e7afbb 100644 --- a/src/tools/list_directory.rs +++ b/src/tools/list_directory.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/list_directory_with_sizes.rs b/src/tools/list_directory_with_sizes.rs index 8929cf9..803d290 100644 --- a/src/tools/list_directory_with_sizes.rs +++ b/src/tools/list_directory_with_sizes.rs @@ -1,11 +1,11 @@ -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use std::fmt::Write; use std::path::Path; -use crate::fs_service::utils::format_bytes; use crate::fs_service::FileSystemService; +use crate::fs_service::utils::format_bytes; #[mcp_tool( name = "list_directory_with_sizes", diff --git a/src/tools/move_file.rs b/src/tools/move_file.rs index adafaa7..3ea5096 100644 --- a/src/tools/move_file.rs +++ b/src/tools/move_file.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/read_files.rs b/src/tools/read_files.rs index e4bc34a..a6c91d1 100644 --- a/src/tools/read_files.rs +++ b/src/tools/read_files.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/read_multiple_files.rs b/src/tools/read_multiple_files.rs index 6df881f..fc903c2 100644 --- a/src/tools/read_multiple_files.rs +++ b/src/tools/read_multiple_files.rs @@ -1,9 +1,9 @@ use std::path::Path; use futures::future::join_all; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/src/tools/search_file.rs b/src/tools/search_file.rs index dfee718..510ae99 100644 --- a/src/tools/search_file.rs +++ b/src/tools/search_file.rs @@ -1,8 +1,8 @@ use std::path::Path; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; #[mcp_tool( @@ -41,6 +41,7 @@ impl SearchFilesTool { params.pattern, params.exclude_patterns.unwrap_or_default(), ) + .await .map_err(CallToolError::new)?; let result = if !list.is_empty() { diff --git a/src/tools/search_files_content.rs b/src/tools/search_files_content.rs index e1e50e9..f5cf9cf 100644 --- a/src/tools/search_files_content.rs +++ b/src/tools/search_files_content.rs @@ -1,8 +1,8 @@ use crate::error::ServiceError; use crate::fs_service::{FileSearchResult, FileSystemService}; -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use std::fmt::Write; #[mcp_tool( name = "search_files_content", @@ -65,13 +65,16 @@ impl SearchFilesContentTool { context: &FileSystemService, ) -> std::result::Result { let is_regex = params.is_regex.unwrap_or_default(); - match context.search_files_content( - ¶ms.path, - ¶ms.pattern, - ¶ms.query, - is_regex, - params.exclude_patterns.to_owned(), - ) { + match context + .search_files_content( + ¶ms.path, + ¶ms.pattern, + ¶ms.query, + is_regex, + params.exclude_patterns.to_owned(), + ) + .await + { Ok(results) => { if results.is_empty() { return Ok(CallToolResult::with_error(CallToolError::new( diff --git a/src/tools/write_file.rs b/src/tools/write_file.rs index f323950..52797cb 100644 --- a/src/tools/write_file.rs +++ b/src/tools/write_file.rs @@ -1,10 +1,10 @@ use rust_mcp_sdk::{ - macros::{mcp_tool, JsonSchema}, + macros::{JsonSchema, mcp_tool}, schema::TextContent, }; use std::path::Path; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; #[mcp_tool( diff --git a/src/tools/zip_unzip.rs b/src/tools/zip_unzip.rs index 80a3e7c..88f77a1 100644 --- a/src/tools/zip_unzip.rs +++ b/src/tools/zip_unzip.rs @@ -1,6 +1,6 @@ -use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::macros::{JsonSchema, mcp_tool}; use rust_mcp_sdk::schema::TextContent; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError}; use crate::fs_service::FileSystemService; diff --git a/tests/common/common.rs b/tests/common/common.rs index 0ff88f6..8650026 100644 --- a/tests/common/common.rs +++ b/tests/common/common.rs @@ -2,12 +2,13 @@ use std::{ fs::{self, File}, io::Write, path::{Path, PathBuf}, + sync::Arc, }; use clap::Parser; use rust_mcp_filesystem::{ cli::CommandArguments, - fs_service::{file_info::FileInfo, FileSystemService}, + fs_service::{FileSystemService, file_info::FileInfo}, }; use tempfile::TempDir; @@ -18,7 +19,7 @@ pub fn get_temp_dir() -> PathBuf { } // Helper to create a FileSystemService with temporary directories -pub fn setup_service(dirs: Vec) -> (PathBuf, FileSystemService) { +pub fn setup_service(dirs: Vec) -> (PathBuf, FileSystemService, Arc>) { let temp_dir = get_temp_dir(); let allowed_dirs = dirs .into_iter() @@ -30,7 +31,8 @@ pub fn setup_service(dirs: Vec) -> (PathBuf, FileSystemService) { }) .collect::>(); let service = FileSystemService::try_new(&allowed_dirs).unwrap(); - (temp_dir, service) + let allowed_dirs: Vec = allowed_dirs.iter().map(|i| i.into()).collect(); + (temp_dir, service, Arc::new(allowed_dirs)) } // Helper to create a temporary file diff --git a/tests/test_cli.rs b/tests/test_cli.rs index 0c950a5..ab521ed 100644 --- a/tests/test_cli.rs +++ b/tests/test_cli.rs @@ -38,11 +38,15 @@ fn test_parse_with_write_flag_long() { #[test] fn test_missing_required_directories() { let args = ["mcp-server"]; + + // parse should pass let result = parse_args(&args); - assert!(result.is_err()); - if let Err(e) = result { - assert_eq!(e.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } + assert!(result.is_ok()); + + let result = result.unwrap().validate(); + assert!( + matches!(result, Err(message) if message.contains("is required when `--enable-roots` is not provided")) + ); } #[test] diff --git a/tests/test_fs_service.rs b/tests/test_fs_service.rs index 456ecac..0262115 100644 --- a/tests/test_fs_service.rs +++ b/tests/test_fs_service.rs @@ -10,9 +10,9 @@ use common::setup_service; use dirs::home_dir; use grep::matcher::Match; use rust_mcp_filesystem::error::ServiceError; +use rust_mcp_filesystem::fs_service::FileSystemService; use rust_mcp_filesystem::fs_service::file_info::FileInfo; use rust_mcp_filesystem::fs_service::utils::*; -use rust_mcp_filesystem::fs_service::FileSystemService; use rust_mcp_filesystem::tools::EditOperation; use std::fs::{self, File}; use std::io::Write; @@ -24,15 +24,15 @@ use tokio_util::compat::TokioAsyncReadCompatExt; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -#[test] -fn test_try_new_success() { +#[tokio::test] +async fn test_try_new_success() { let temp_dir = get_temp_dir(); let dir_path = temp_dir.to_str().unwrap().to_string(); let result = FileSystemService::try_new(&[dir_path]); assert!(result.is_ok()); let service = result.unwrap(); - assert_eq!(service.allowed_directories().as_ref(), vec![temp_dir]); + assert_eq!(*service.allowed_directories().await, vec![temp_dir]); } #[test] @@ -41,29 +41,29 @@ fn test_try_new_invalid_directory() { let _ = FileSystemService::try_new(&["/does/not/exist".to_string()]); } -#[test] -fn test_allowed_directories() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); - let allowed = service.allowed_directories(); +#[tokio::test] +async fn test_allowed_directories() { + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); + let allowed = service.allowed_directories().await; assert_eq!(allowed.len(), 1); assert_eq!(allowed[0], temp_dir.join("dir1")); } #[tokio::test] async fn test_validate_path_allowed() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = temp_dir.join("dir1").join("test.txt"); create_temp_file(temp_dir.join("dir1").as_path(), "test.txt", "content"); - let result = service.validate_path(&file_path); + let result = service.validate_path(&file_path, allowed_dirs); assert!(result.is_ok()); assert_eq!(result.unwrap(), file_path); } #[tokio::test] async fn test_validate_path_denied() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, allowed_dirs) = setup_service(vec!["dir1".to_string()]); let outside_path = temp_dir.join("dir2").join("test.txt"); - let result = service.validate_path(&outside_path); + let result = service.validate_path(&outside_path, allowed_dirs); assert!(matches!(result, Err(ServiceError::FromString(_)))); } @@ -98,7 +98,7 @@ fn test_contains_symlink_with_symlink() { #[tokio::test] async fn test_get_file_stats() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file(temp_dir.join("dir1").as_path(), "test.txt", "content"); let result = service.get_file_stats(&file_path).await.unwrap(); assert_eq!(result.size, 7); // "content" is 7 bytes @@ -111,7 +111,7 @@ async fn test_get_file_stats() { #[tokio::test] async fn test_zip_directory() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); create_temp_file(&dir_path, "file1.txt", "content1"); @@ -132,7 +132,7 @@ async fn test_zip_directory() { #[tokio::test] async fn test_zip_directory_already_exists() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); let zip_path = create_temp_file(&dir_path, "output.zip", "dummy"); let result = service @@ -150,7 +150,7 @@ async fn test_zip_directory_already_exists() { #[tokio::test] async fn test_zip_files() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); let file1 = create_temp_file(dir_path.as_path(), "file1.txt", "content1"); @@ -173,7 +173,7 @@ async fn test_zip_files() { #[tokio::test] async fn test_zip_files_empty_input() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let zip_path = temp_dir.join("output.zip"); let result = service .zip_files(vec![], zip_path.to_str().unwrap().to_string()) @@ -186,7 +186,7 @@ async fn test_zip_files_empty_input() { #[tokio::test] async fn test_unzip_file() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); let file1 = create_temp_file(&dir_path, "file1.txt", "content1"); let zip_path = dir_path.join("output.zip"); @@ -208,7 +208,7 @@ async fn test_unzip_file() { #[tokio::test] async fn test_unzip_file_non_existent() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let temp_dir = temp_dir.join("dir1"); let zip_path = temp_dir.join("non_existent.zip"); let extract_dir = temp_dir.join("extracted"); @@ -224,7 +224,7 @@ async fn test_unzip_file_non_existent() { #[tokio::test] async fn test_read_file() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file(temp_dir.join("dir1").as_path(), "test.txt", "content"); let content = service.read_file(&file_path).await.unwrap(); assert_eq!(content, "content"); @@ -232,7 +232,7 @@ async fn test_read_file() { #[tokio::test] async fn test_create_directory() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let new_dir = temp_dir.join("dir1").join("new_dir"); let result = service.create_directory(&new_dir).await; @@ -242,7 +242,7 @@ async fn test_create_directory() { #[tokio::test] async fn test_move_file() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let src_path = create_temp_file(temp_dir.join("dir1").as_path(), "src.txt", "content"); let dest_path = temp_dir.join("dir1").join("dest.txt"); let result = service.move_file(&src_path, &dest_path).await; @@ -253,7 +253,7 @@ async fn test_move_file() { #[tokio::test] async fn test_list_directory() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); create_temp_file(&dir_path, "file1.txt", "content1"); create_temp_file(&dir_path, "file2.txt", "content2"); @@ -269,7 +269,7 @@ async fn test_list_directory() { #[tokio::test] async fn test_write_file() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = temp_dir.join("dir1").join("test.txt"); let content = "new content".to_string(); let result = service.write_file(&file_path, &content).await; @@ -277,14 +277,15 @@ async fn test_write_file() { assert_eq!(tokio_fs::read_to_string(&file_path).await.unwrap(), content); } -#[test] -fn test_search_files() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); +#[tokio::test] +async fn test_search_files() { + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); create_temp_file(&dir_path, "test1.txt", "content"); create_temp_file(&dir_path, "test2.doc", "content"); let result = service .search_files(&dir_path, "*.txt".to_string(), vec![]) + .await .unwrap(); let names: Vec<_> = result .into_iter() @@ -293,9 +294,9 @@ fn test_search_files() { assert_eq!(names, vec!["test1.txt"]); } -#[test] -fn test_search_files_with_exclude() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); +#[tokio::test] +async fn test_search_files_with_exclude() { + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let dir_path = temp_dir.join("dir1"); create_temp_file(&dir_path, "test1.txt", "content"); create_temp_file(&dir_path, "test2.txt", "content"); @@ -305,6 +306,7 @@ fn test_search_files_with_exclude() { "*.txt".to_string(), vec!["test2.txt".to_string()], ) + .await .unwrap(); let names: Vec<_> = result .into_iter() @@ -315,7 +317,7 @@ fn test_search_files_with_exclude() { #[test] fn test_create_unified_diff() { - let (_, service) = setup_service(vec![]); + let (_, service, _) = setup_service(vec![]); let original = "line1\nline2\nline3".to_string(); let new = "line1\nline4\nline3".to_string(); let diff = service.create_unified_diff(&original, &new, Some("test.txt".to_string())); @@ -328,7 +330,7 @@ fn test_create_unified_diff() { #[tokio::test] async fn test_apply_file_edits() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file( temp_dir.join("dir1").as_path(), "test.txt", @@ -351,7 +353,7 @@ async fn test_apply_file_edits() { #[tokio::test] async fn test_apply_file_edits_dry_run() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file( temp_dir.join("dir1").as_path(), "test.txt", @@ -374,7 +376,7 @@ async fn test_apply_file_edits_dry_run() { #[tokio::test] async fn test_apply_file_edits_no_match() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file( temp_dir.join("dir1").as_path(), "test.txt", @@ -592,7 +594,7 @@ fn test_display_format_for_empty_timestamps() { #[tokio::test] async fn test_apply_file_edits_mixed_indentation() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file( temp_dir.join("dir1").as_path(), "test_indent.txt", @@ -640,7 +642,7 @@ async fn test_apply_file_edits_mixed_indentation() { #[tokio::test] async fn test_apply_file_edits_mixed_indentation_2() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file_path = create_temp_file( temp_dir.join("dir1").as_path(), "test_indent.txt", @@ -687,7 +689,7 @@ async fn test_apply_file_edits_mixed_indentation_2() { #[tokio::test] async fn test_exact_match() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), @@ -712,7 +714,7 @@ async fn test_exact_match() { #[tokio::test] async fn test_exact_match_edit2() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file1.txt", @@ -735,7 +737,7 @@ async fn test_exact_match_edit2() { #[tokio::test] async fn test_line_by_line_match_with_indent() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file2.rs", @@ -760,7 +762,7 @@ async fn test_line_by_line_match_with_indent() { #[tokio::test] async fn test_dry_run_mode() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file4.sh", @@ -783,7 +785,7 @@ async fn test_dry_run_mode() { #[tokio::test] async fn test_save_to_different_path() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let orig_file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file5.txt", @@ -811,7 +813,7 @@ async fn test_save_to_different_path() { #[tokio::test] async fn test_diff_backtick_formatting() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file6.md", @@ -835,7 +837,7 @@ async fn test_diff_backtick_formatting() { #[tokio::test] async fn test_no_edits_provided() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file7.toml", @@ -853,7 +855,7 @@ async fn test_no_edits_provided() { #[tokio::test] async fn test_preserve_windows_line_endings() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "test_file.txt", @@ -876,7 +878,7 @@ async fn test_preserve_windows_line_endings() { #[tokio::test] async fn test_preserve_unix_line_endings() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir1"), "unix_line_file.txt", @@ -901,7 +903,7 @@ async fn test_preserve_unix_line_endings() { #[tokio::test] // Issue #19: https://github.com/rust-mcp-stack/rust-mcp-filesystem/issues/19 async fn test_panic_on_out_of_bounds_edit() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); // Set up an edit that expects to match 5 lines let edit = EditOperation { @@ -927,7 +929,7 @@ async fn test_panic_on_out_of_bounds_edit() { #[tokio::test] async fn test_content_search() { - let (temp_dir, service) = setup_service(vec!["dir_search".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir_search".to_string()]); let file = create_temp_file( &temp_dir.as_path().join("dir_search"), "file_to_search.txt", @@ -974,7 +976,7 @@ async fn test_content_search() { #[test] fn test_match_near_start_short_line() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); let line = "match this text"; let m = Match::new(0, 5); @@ -987,7 +989,7 @@ fn test_match_near_start_short_line() { #[tokio::test] async fn test_snippet_back_chars() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); let line = "this is a long enough line for testing match in middle"; let m = Match::new(40, 45); let result = service.extract_snippet(line, m, Some(20), Some(5)); @@ -1007,7 +1009,7 @@ async fn test_snippet_back_chars() { #[test] fn test_match_triggers_only_end_ellipsis() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); let line = "match is at start but line is long"; let m = Match::new(0, 5); @@ -1021,7 +1023,7 @@ fn test_match_triggers_only_end_ellipsis() { #[test] fn test_match_triggers_only_start_ellipsis() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); let line = "line is long and match is near end"; let m = Match::new(31, 36); @@ -1033,7 +1035,7 @@ fn test_match_triggers_only_start_ellipsis() { #[test] fn test_trim_applied() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); let line = " match here with spaces "; let m = Match::new(5, 10); @@ -1047,7 +1049,7 @@ fn test_trim_applied() { #[test] fn test_exact_snippet_end() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _allowed_dirs) = setup_service(vec!["dir_search".to_string()]); let line = "some content with match inside"; let m = Match::new(18, 23); let result = service.extract_snippet(line, m, Some(line.len()), Some(18)); @@ -1055,9 +1057,9 @@ fn test_exact_snippet_end() { assert_eq!(result, "some content with match inside"); } -#[test] -fn search_files_content() { - let (temp_dir, service) = setup_service(vec!["dir_search".to_string()]); +#[tokio::test] +async fn search_files_content() { + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir_search".to_string()]); create_temp_file( &temp_dir.as_path().join("dir_search"), @@ -1090,6 +1092,7 @@ fn search_files_content() { true, None, ) + .await .unwrap(); assert_eq!(results.len(), 2); assert_eq!(results[0].matches.len(), 2); @@ -1098,7 +1101,7 @@ fn search_files_content() { #[test] fn test_extract_snippet_bug_37() { - let (_, service) = setup_service(vec!["dir_search".to_string()]); + let (_, service, _) = setup_service(vec!["dir_search".to_string()]); // Input string : ’ starts spans 3 bytes: 0xE2 0x80 0x99. let line = "If and when that happens, however, we will not be able to declare victory quite yet. Defeating the open conspiracy to deprive students of physical access to books will do little to counteract the more diffuse confluence of forces that are depriving students of their education with a curly apostrophe ’ followed by more text"; diff --git a/tests/test_tools.rs b/tests/test_tools.rs index 8c46611..542fff1 100644 --- a/tests/test_tools.rs +++ b/tests/test_tools.rs @@ -3,12 +3,12 @@ pub mod common; use common::setup_service; use rust_mcp_filesystem::tools::*; -use rust_mcp_sdk::schema::{schema_utils::CallToolError, ContentBlock}; +use rust_mcp_sdk::schema::{ContentBlock, schema_utils::CallToolError}; use std::fs; #[tokio::test] async fn test_create_directory_new_directory() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let new_dir = temp_dir.join("dir1").join("new_dir"); let params = CreateDirectoryTool { path: new_dir.to_str().unwrap().to_string(), @@ -39,7 +39,7 @@ async fn test_create_directory_new_directory() { #[tokio::test] async fn test_create_directory_existing_directory() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let existing_dir = temp_dir.join("dir1").join("existing_dir"); fs::create_dir_all(&existing_dir).unwrap(); let params = CreateDirectoryTool { @@ -71,7 +71,7 @@ async fn test_create_directory_existing_directory() { #[tokio::test] async fn test_create_directory_nested() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let nested_dir = temp_dir.join("dir1").join("nested/subdir"); let params = CreateDirectoryTool { path: nested_dir.to_str().unwrap().to_string(), @@ -100,7 +100,7 @@ async fn test_create_directory_nested() { #[tokio::test] async fn test_create_directory_outside_allowed() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let outside_dir = temp_dir.join("dir2").join("forbidden"); let params = CreateDirectoryTool { path: outside_dir.to_str().unwrap().to_string(), @@ -115,7 +115,7 @@ async fn test_create_directory_outside_allowed() { #[tokio::test] async fn test_create_directory_invalid_path() { - let (temp_dir, service) = setup_service(vec!["dir1".to_string()]); + let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]); let invalid_path = temp_dir.join("dir1").join("invalid\0dir"); let params = CreateDirectoryTool { path: invalid_path