diff --git a/mcp-client-rust/Cargo.lock b/mcp-client-rust/Cargo.lock index 88d50622..4ca977d7 100644 --- a/mcp-client-rust/Cargo.lock +++ b/mcp-client-rust/Cargo.lock @@ -17,6 +17,20 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +40,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -41,6 +61,17 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -74,18 +105,45 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" version = "1.11.1" @@ -121,10 +179,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link 0.2.1", ] @@ -150,8 +206,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -165,7 +231,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", ] [[package]] @@ -174,9 +253,20 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -215,7 +305,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -226,7 +316,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "unicode-xid", ] @@ -238,7 +328,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -253,6 +343,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -289,6 +388,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -301,12 +411,29 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -331,6 +458,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures" version = "0.3.31" @@ -387,7 +524,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -456,24 +593,35 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "gimli" version = "0.32.3" @@ -510,6 +658,11 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hex" @@ -650,7 +803,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -841,6 +994,34 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0303b14f91cbac17c64aaf2ef60ab71fe5f34c3867cedcbca72c9dd15f5040fe" +dependencies = [ + "ahash", + "base64", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "reqwest", + "serde", + "serde_json", + "unicode-general-category", + "uuid-simd", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -849,9 +1030,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "linux-raw-sys" @@ -902,6 +1083,7 @@ dependencies = [ "anyhow", "dotenvy", "genai", + "jsonschema", "reqwest", "rmcp", "serde_json", @@ -944,7 +1126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] @@ -967,9 +1149,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -996,12 +1178,81 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1048,7 +1299,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1069,6 +1320,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1093,10 +1350,10 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -1157,9 +1414,9 @@ dependencies = [ [[package]] name = "process-wrap" -version = "8.2.1" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" dependencies = [ "futures", "indexmap 2.11.4", @@ -1196,7 +1453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand", "ring", @@ -1239,6 +1496,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.4" @@ -1265,7 +1528,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -1294,14 +1557,41 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "referencing" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d0d0665043906aacf1d83bea9d61e5134f8f437815b84320e7facf8ff4e9c2" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.0", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -1323,6 +1613,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -1393,14 +1684,15 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.8.1" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35acda8f89fca5fd8c96cae3c6d5b4c38ea0072df4c8030915f3b5ff469c1c" +checksum = "3875f7c471c2d709062bc2165d8dc883b021b44255bd00bb4d8025f5108178c8" dependencies = [ + "async-trait", "base64", "chrono", "futures", - "paste", + "pastey", "pin-project-lite", "process-wrap", "rmcp-macros", @@ -1412,19 +1704,20 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "0.8.1" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f1d5220aaa23b79c3d02e18f7a554403b3ccea544bbb6c69d6bcb3e854a274" +checksum = "37a5ce7541aff2f3df9e8c3c48c7241800fa6fa68e2fbcba386c0d7dcdfc953c" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.106", ] [[package]] @@ -1543,7 +1836,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.106", ] [[package]] @@ -1602,7 +1895,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1613,7 +1906,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1666,10 +1959,10 @@ version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1747,6 +2040,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -1764,7 +2068,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1795,7 +2099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1827,7 +2131,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1838,7 +2142,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1934,7 +2238,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2045,7 +2349,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2093,6 +2397,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -2129,6 +2439,27 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2152,6 +2483,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "want" version = "0.3.1" @@ -2167,15 +2510,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -2208,7 +2542,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -2243,7 +2577,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2301,37 +2635,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "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]] -name = "windows-core" -version = "0.61.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -2349,12 +2669,12 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core", + "windows-link 0.2.1", "windows-threading", ] @@ -2366,7 +2686,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2377,7 +2697,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2394,12 +2714,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core", + "windows-link 0.2.1", ] [[package]] @@ -2494,11 +2814,11 @@ dependencies = [ [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] @@ -2581,7 +2901,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "synstructure", ] @@ -2602,7 +2922,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2622,7 +2942,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "synstructure", ] @@ -2662,5 +2982,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] diff --git a/mcp-client-rust/Cargo.toml b/mcp-client-rust/Cargo.toml index 6b5e3e5b..9699727b 100644 --- a/mcp-client-rust/Cargo.toml +++ b/mcp-client-rust/Cargo.toml @@ -6,7 +6,8 @@ edition = "2024" [dependencies] anyhow = "1.0.100" genai = "0.4.2" -rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] } +jsonschema = "0.35" +rmcp = { version = "3.0.0-beta.2", features = ["server", "client", "transport-io", "transport-child-process"] } tokio = { version = "1.47.1", features = ['full']} tracing = "0.1.41" tracing-subscriber = {version = "0.3", features = ["env-filter"]} diff --git a/mcp-client-rust/README.md b/mcp-client-rust/README.md index 03ed76ae..ad98e104 100644 --- a/mcp-client-rust/README.md +++ b/mcp-client-rust/README.md @@ -1,3 +1,9 @@ # An LLM-Powered Chatbot MCP Client written in Rust See the [Build an MCP client](https://modelcontextprotocol.io/docs/develop/build-client) tutorial for more information. + +## Structured output + +rmcp does not validate tool output, so this client compiles each declared `outputSchema` at connect time and checks results against it — the spec's client-side SHOULD. It uses the [`jsonschema`](https://docs.rs/jsonschema) crate, which rmcp's own documentation recommends. + +The two channels go to different readers: `content` is forwarded to the model, while `structured_content` is used as data — the client counts the items it returns. See [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content). diff --git a/mcp-client-rust/src/main.rs b/mcp-client-rust/src/main.rs index 3fb7c09b..6472b87c 100644 --- a/mcp-client-rust/src/main.rs +++ b/mcp-client-rust/src/main.rs @@ -3,19 +3,24 @@ use genai::Client; use genai::chat::{ ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse, }; -use rmcp::model::{CallToolRequestParam, Tool as McpTool}; +use jsonschema::Validator; +use rmcp::model::{CallToolRequestParams, CallToolResult, Tool as McpTool}; use rmcp::service::{RoleClient, RunningService, ServiceExt}; use rmcp::transport::TokioChildProcess; use serde_json::Value; +use std::collections::HashMap; use tokio::io::{self, AsyncBufReadExt, BufReader}; use tokio::process::Command; -const MODEL_ANTHROPIC: &str = "claude-sonnet-4-20250514"; +const MODEL_ANTHROPIC: &str = "claude-sonnet-5"; struct MCPClient { anthropic: Client, session: Option>, tools: Vec, + /// Compiled `outputSchema` per tool name. rmcp does not validate results, + /// so this client does it with the `jsonschema` crate. + output_schemas: HashMap, } impl MCPClient { @@ -24,9 +29,28 @@ impl MCPClient { anthropic: Client::default(), session: None, tools: Vec::new(), + output_schemas: HashMap::new(), }) } + /// Check a result against its tool's declared `outputSchema`. Error results + /// are exempt: they carry a message, not data. + fn validate_tool_output(&self, name: &str, result: &CallToolResult) -> Result<()> { + let Some(validator) = self.output_schemas.get(name) else { + return Ok(()); + }; + if result.is_error.unwrap_or(false) { + return Ok(()); + } + let Some(structured) = &result.structured_content else { + bail!("Tool {name} declares an output schema but returned no structured content"); + }; + if let Err(error) = validator.validate(structured) { + bail!("Structured content from tool {name} does not match its output schema: {error}"); + } + Ok(()) + } + async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> { if self.session.is_some() { bail!("Client is already connected to a server"); @@ -52,6 +76,18 @@ impl MCPClient { println!("Connected to server with tools: {tool_names:?}"); + // An outputSchema root may be any JSON Schema, not just an object. + for tool in &rmcp_tools { + let Some(schema) = &tool.output_schema else { + continue; + }; + let schema = Value::Object(schema.as_ref().clone()); + let validator = Validator::new(&schema).with_context(|| { + format!("Failed to compile output schema of tool {}", tool.name) + })?; + self.output_schemas.insert(tool.name.to_string(), validator); + } + self.tools = convert_tools(&rmcp_tools); self.session = Some(session); Ok(()) @@ -93,16 +129,33 @@ impl MCPClient { )); // Query the MCP server + let mut params = CallToolRequestParams::new(tool_call.fn_name.clone()); + if let Some(arguments) = tool_call.fn_arguments.as_object().cloned() { + params = params.with_arguments(arguments); + } let tool_result = session - .call_tool(CallToolRequestParam { - name: tool_call.fn_name.clone().into(), - arguments: tool_call.fn_arguments.as_object().cloned(), - }) + .call_tool(params) .await .with_context(|| format!("Tool call {} failed", tool_call.fn_name))?; - let payload = serde_json::to_string(&tool_result) - .context("Failed to serialize tool result")?; + self.validate_tool_output(&tool_call.fn_name, &tool_result)?; + + // structured_content is data the application can use directly. + if let Some(Value::Array(items)) = &tool_result.structured_content { + final_text.push(format!( + "[{} returned {} items]", + tool_call.fn_name, + items.len() + )); + } + + // content is a list of block types; forward only the text ones. + let payload = tool_result + .content + .iter() + .filter_map(|block| block.as_text().map(|text| text.text.as_str())) + .collect::>() + .join("\n"); tool_results.push(ContentPart::ToolResponse(ToolResponse::new( tool_call.call_id.clone(), @@ -179,7 +232,8 @@ impl MCPClient { #[tokio::main] async fn main() -> Result<()> { - dotenvy::dotenv().context("Failed to load env file")?; + // .env is optional; the key may come from the environment instead. + let _ = dotenvy::dotenv(); let mut args = std::env::args(); let _ = args.next(); @@ -194,6 +248,17 @@ async fn main() -> Result<()> { let result = async { client.connect_to_server(&server_args).await?; + + // Connecting and listing tools needs no credentials; querying them + // does. Matching the Python and TypeScript clients, report and exit + // rather than failing, so the connection itself can be exercised + // without a key. + if std::env::var("ANTHROPIC_API_KEY").is_err() { + println!("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:"); + println!(" export ANTHROPIC_API_KEY=your-api-key-here"); + return Ok(()); + } + client.chat_loop().await } .await; diff --git a/weather-server-rust/Cargo.lock b/weather-server-rust/Cargo.lock index 2cbc9f19..de61996e 100644 --- a/weather-server-rust/Cargo.lock +++ b/weather-server-rust/Cargo.lock @@ -47,6 +47,17 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -121,10 +132,8 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link", ] @@ -146,9 +155,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "darling" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -156,27 +165,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.104", ] [[package]] name = "darling_macro" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -187,7 +195,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -313,7 +321,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -365,10 +373,21 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "gimli" version = "0.31.1" @@ -861,7 +880,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -906,10 +925,10 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -968,6 +987,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.5.15" @@ -994,7 +1019,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1070,14 +1095,15 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.3.0" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b008b927a85d514699ff304be84c5084598596e6cad4a6f5bc67207715fafe5f" +checksum = "3875f7c471c2d709062bc2165d8dc883b021b44255bd00bb4d8025f5108178c8" dependencies = [ + "async-trait", "base64", "chrono", "futures", - "paste", + "pastey", "pin-project-lite", "rmcp-macros", "schemars", @@ -1087,19 +1113,20 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "0.3.0" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7465280d5f73f2c5c99017a04af407b2262455a149f255ad22f2b0b29087695c" +checksum = "37a5ce7541aff2f3df9e8c3c48c7241800fa6fa68e2fbcba386c0d7dcdfc953c" dependencies = [ "darling", "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.104", ] [[package]] @@ -1198,7 +1225,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.104", ] [[package]] @@ -1247,7 +1274,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1258,7 +1285,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1360,6 +1387,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -1377,7 +1415,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1431,7 +1469,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1481,7 +1519,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1581,7 +1619,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1658,6 +1696,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1716,7 +1765,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -1751,7 +1800,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1810,7 +1859,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1821,7 +1870,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2049,7 +2098,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "synstructure", ] @@ -2070,7 +2119,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "synstructure", ] @@ -2110,5 +2159,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] diff --git a/weather-server-rust/Cargo.toml b/weather-server-rust/Cargo.toml index 52471f1c..b97b6d15 100644 --- a/weather-server-rust/Cargo.toml +++ b/weather-server-rust/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] } +rmcp = { version = "3.0.0-beta.2", features = ["server", "macros", "transport-io"] } tokio = { version = "1.46", features = ["full"] } reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0", features = ["derive"] } diff --git a/weather-server-rust/README.md b/weather-server-rust/README.md index 4d819f05..a0d5bc61 100644 --- a/weather-server-rust/README.md +++ b/weather-server-rust/README.md @@ -1,3 +1,11 @@ # A Simple MCP Weather Server written in Rust See the [Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) tutorial for more information. + +## Structured output + +Both tools declare an `output_schema` and return `structured_content`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content) in the spec. + +Note that an array-rooted schema requires a `2026-07-28` client: rmcp sends it as written on every connection rather than projecting it down, so an older client rejects the tool list. + +`list_tools` is hand-written because `#[tool_handler]` omits the SEP-2549 `ttl_ms` and `cache_scope` fields, which are required on a paginated result at this revision. diff --git a/weather-server-rust/src/main.rs b/weather-server-rust/src/main.rs index 96192fd9..762e31bc 100644 --- a/weather-server-rust/src/main.rs +++ b/weather-server-rust/src/main.rs @@ -1,12 +1,15 @@ use anyhow::Result; use rmcp::{ - ServerHandler, ServiceExt, - handler::server::{router::tool::ToolRouter, tool::Parameters}, + ErrorData, ServerHandler, ServiceExt, + handler::server::{router::tool::ToolRouter, tool::schema_for_output, wrapper::Parameters}, model::*, - schemars, tool, tool_handler, tool_router, + schemars::{self, JsonSchema}, + tool, tool_handler, tool_router, + transport::stdio, }; use serde::Deserialize; use serde::de::DeserializeOwned; +use serde::Serialize; const NWS_API_BASE: &str = "https://api.weather.gov"; const USER_AGENT: &str = "weather-app/1.0"; @@ -65,6 +68,44 @@ struct ForecastPeriod { detailed_forecast: String, } +/// One element of `get_alerts`' structured output. Declaring `Vec` as +/// the output schema makes `structuredContent` a top-level JSON array rather +/// than an object wrapping one, which revision 2026-07-28 is the first to allow. +#[derive(Debug, Serialize, JsonSchema)] +pub struct Alert { + /// The kind of weather event. + event: String, + /// The area the alert covers. + area: String, + /// How severe the event is. + severity: String, + /// What is happening. + description: String, + /// What people in the area should do. + instructions: String, +} + +/// `get_forecast`' structured output: the object case. +#[derive(Debug, Serialize, JsonSchema)] +pub struct Forecast { + /// Latitude the forecast is for. + latitude: f32, + /// Longitude the forecast is for. + longitude: f32, + /// The forecast periods, soonest first. + periods: Vec, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct Period { + name: String, + temperature: i32, + temperature_unit: String, + wind_speed: String, + wind_direction: String, + detailed_forecast: String, +} + async fn make_nws_request(url: &str) -> Result { let client = reqwest::Client::new(); let rsp = client @@ -77,25 +118,14 @@ async fn make_nws_request(url: &str) -> Result { Ok(rsp.json::().await?) } -fn format_alert(feature: &AlertFeature) -> String { - let props = &feature.properties; +fn format_alert(alert: &Alert) -> String { format!( "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}", - props.event.as_deref().unwrap_or("Unknown"), - props.area_desc.as_deref().unwrap_or("Unknown"), - props.severity.as_deref().unwrap_or("Unknown"), - props - .description - .as_deref() - .unwrap_or("No description available"), - props - .instruction - .as_deref() - .unwrap_or("No specific instructions provided") + alert.event, alert.area, alert.severity, alert.description, alert.instructions ) } -fn format_period(period: &ForecastPeriod) -> String { +fn format_period(period: &Period) -> String { format!( "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}", period.name, @@ -107,22 +137,39 @@ fn format_period(period: &ForecastPeriod) -> String { ) } -#[derive(serde::Deserialize, schemars::JsonSchema)] +/// Build a result carrying both channels. +fn dual_channel_result( + text: String, + structured: impl Serialize, +) -> Result { + let value = serde_json::to_value(structured).map_err(|e| { + ErrorData::internal_error(format!("failed to serialize structured content: {e}"), None) + })?; + let mut result = CallToolResult::success(vec![ContentBlock::text(text)]); + result.structured_content = Some(value); + Ok(result) +} + +#[derive(serde::Deserialize, JsonSchema)] pub struct MCPForecastRequest { + /// Latitude of the location. latitude: f32, + /// Longitude of the location. longitude: f32, } -#[derive(serde::Deserialize, schemars::JsonSchema)] +#[derive(serde::Deserialize, JsonSchema)] pub struct MCPAlertRequest { + /// Two-letter US state code (e.g. CA, NY). state: String, } +#[derive(Clone)] pub struct Weather { tool_router: ToolRouter, } -#[tool_router] +#[tool_router(router = tool_router)] impl Weather { fn new() -> Self { Self { @@ -130,77 +177,135 @@ impl Weather { } } - #[tool(description = "Get weather alerts for a US state.")] + #[tool( + description = "Get weather alerts for a US state.", + output_schema = schema_for_output::>() + )] async fn get_alerts( &self, Parameters(MCPAlertRequest { state }): Parameters, - ) -> String { - let url = format!( - "{}/alerts/active/area/{}", - NWS_API_BASE, - state.to_uppercase() - ); - - match make_nws_request::(&url).await { - Ok(data) => { - if data.features.is_empty() { - "No active alerts for this state.".to_string() - } else { - data.features - .iter() - .map(format_alert) - .collect::>() - .join("\n---\n") + ) -> Result { + let state = state.to_uppercase(); + let url = format!("{NWS_API_BASE}/alerts/active/area/{state}"); + + let Ok(data) = make_nws_request::(&url).await else { + return Ok(CallToolResult::error(vec![ContentBlock::text(format!( + "Unable to fetch alerts for {state}." + ))])); + }; + + // An empty result is an empty array, not an error. + let alerts: Vec = data + .features + .into_iter() + .map(|feature| { + let props = feature.properties; + Alert { + event: props.event.unwrap_or_else(|| "Unknown".into()), + area: props.area_desc.unwrap_or_else(|| "Unknown".into()), + severity: props.severity.unwrap_or_else(|| "Unknown".into()), + description: props + .description + .unwrap_or_else(|| "No description available".into()), + instructions: props + .instruction + .unwrap_or_else(|| "No specific instructions provided".into()), } - } - Err(_) => "Unable to fetch alerts or no alerts found.".to_string(), - } + }) + .collect(); + + let text = if alerts.is_empty() { + "No active alerts for this state.".to_string() + } else { + alerts + .iter() + .map(format_alert) + .collect::>() + .join("\n---\n") + }; + + dual_channel_result(text, &alerts) } - #[tool(description = "Get weather forecast for a location.")] + #[tool( + description = "Get weather forecast for a location.", + output_schema = schema_for_output::() + )] async fn get_forecast( &self, Parameters(MCPForecastRequest { latitude, longitude, }): Parameters, - ) -> String { + ) -> Result { let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}"); let Ok(points_data) = make_nws_request::(&points_url).await else { - return "Unable to fetch forecast data for this location.".to_string(); + return Ok(CallToolResult::error(vec![ContentBlock::text( + "Unable to fetch forecast data for this location.", + )])); }; let forecast_url = points_data.properties.forecast; - let Ok(forecast_data) = make_nws_request::(&forecast_url).await else { - return "Unable to fetch forecast data for this location.".to_string(); + return Ok(CallToolResult::error(vec![ContentBlock::text( + "Unable to fetch detailed forecast.", + )])); + }; + + let forecast = Forecast { + latitude, + longitude, + periods: forecast_data + .properties + .periods + .into_iter() + .take(5) // Next 5 periods only + .map(|p| Period { + name: p.name, + temperature: p.temperature, + temperature_unit: p.temperature_unit, + wind_speed: p.wind_speed, + wind_direction: p.wind_direction, + detailed_forecast: p.detailed_forecast, + }) + .collect(), }; - let periods = &forecast_data.properties.periods; - let forecast_summary: String = periods + let text = forecast + .periods .iter() - .take(5) // Next 5 periods only .map(format_period) - .collect::>() + .collect::>() .join("\n---\n"); - forecast_summary + + dual_channel_result(text, &forecast) } } -#[tool_handler] +#[tool_handler(router = self.tool_router)] impl ServerHandler for Weather { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..Default::default() - } + let mut info = ServerInfo::default(); + info.capabilities = ServerCapabilities::builder().enable_tools().build(); + info + } + + // Hand-written because `#[tool_handler]` generates `ttl_ms: None, + // cache_scope: None`, and both are required at revision 2026-07-28. + async fn list_tools( + &self, + _request: Option, + _context: rmcp::service::RequestContext, + ) -> Result { + Ok(ListToolsResult::with_all_items(self.tool_router.list_all()) + .with_ttl_ms(60_000) + .with_cache_scope(CacheScope::Public)) } } #[tokio::main] async fn main() -> Result<()> { - let transport = (tokio::io::stdin(), tokio::io::stdout()); - let service = Weather::new().serve(transport).await?; + let service = Weather::new().serve(stdio()).await?; service.waiting().await?; Ok(()) }