From 65d1a92cf873ab92dcd6031e5bcc5069d0cbbf76 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 25 Jul 2026 17:56:06 -0700 Subject: [PATCH 1/4] Add structured output to the Rust weather server and client Both tools declare an output_schema and return structured_content alongside the text. get_alerts declares Vec, so it answers with a top-level JSON array rather than an array nested in an object, which protocol revision 2026-07-28 is the first to allow. "No alerts" is simply []. get_forecast returns an object, for contrast. Error paths return a tool-level error: a tool declaring an output_schema MUST return conforming structured content, so a path with no data has to fail. The client compiles every declared outputSchema at connect time and validates results against it. rmcp does not do this; its docs point at the jsonschema crate, which is the one new dependency here. Each channel goes to its stated reader: content is forwarded to the model, structured_content is used as data, reporting how many items came back. A missing .env is no longer fatal, and the API key is checked after connecting rather than before. This supersedes #143, which bumped rmcp 0.3 -> 1.4 and failed CI because the 0.3 #[tool] macro API does not survive the jump. The rewrite was required either way, so the bump is folded in and taken to 3.0.0-beta.2, the first version with the 2026-07-28 model. list_tools is hand-written. #[tool_handler] generates one hardcoding ttl_ms: None, cache_scope: None, but both are required on a paginated result at 2026-07-28 (SEP-2549) and a strict client rejects the response outright. get_info does not pin ProtocolVersion::V_2026_07_28 either: rmcp's server never implements server/discover, so pinning would overstate support. Model identifier moves to claude-sonnet-5, replacing claude-sonnet-4-20250514, which is past end of life. Note that rmcp sends an array-rooted schema as written on every connection rather than projecting it down, so an older client rejects the tool list. Co-Authored-By: Claude Opus 5 --- mcp-client-rust/Cargo.lock | 518 ++++++++++++++++++++++++++------ mcp-client-rust/Cargo.toml | 3 +- mcp-client-rust/README.md | 6 + mcp-client-rust/src/main.rs | 83 ++++- weather-server-rust/Cargo.lock | 127 +++++--- weather-server-rust/Cargo.toml | 2 +- weather-server-rust/README.md | 8 + weather-server-rust/src/main.rs | 221 ++++++++++---- 8 files changed, 761 insertions(+), 207 deletions(-) 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(()) } From d6cf28629f59e3d9168b43c82472286c7f3b13c7 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 1 Aug 2026 11:08:28 -0700 Subject: [PATCH 2/4] Update to rmcp 3.1.0 rmcp left beta on 2026-07-28 and reached 3.1.0 on 2026-07-31, so the pin moves off 3.0.0-beta.2. get_info now sets server_info. ServerInfo::default() reports rmcp own crate name and version, so the server was identifying itself as rmcp 3.1.0 rather than weather 1.0.0 as the other three quickstarts do. The hand-written list_tools stays: rmcp-macros 3.1.0 still generates ttl_ms: None, cache_scope: None, which a strict client rejects. Co-Authored-By: Claude Opus 5 --- mcp-client-rust/Cargo.lock | 24 +++++++++++++++--------- mcp-client-rust/Cargo.toml | 2 +- weather-server-rust/Cargo.lock | 20 +++++++++++++------- weather-server-rust/Cargo.toml | 2 +- weather-server-rust/src/main.rs | 3 +++ 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/mcp-client-rust/Cargo.lock b/mcp-client-rust/Cargo.lock index 4ca977d7..7b304cd0 100644 --- a/mcp-client-rust/Cargo.lock +++ b/mcp-client-rust/Cargo.lock @@ -105,6 +105,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "bit-set" version = "0.8.0" @@ -771,7 +777,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1001,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0303b14f91cbac17c64aaf2ef60ab71fe5f34c3867cedcbca72c9dd15f5040fe" dependencies = [ "ahash", - "base64", + "base64 0.22.1", "bytecount", "email_address", "fancy-regex", @@ -1610,7 +1616,7 @@ version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -1684,12 +1690,12 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.0.0-beta.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3875f7c471c2d709062bc2165d8dc883b021b44255bd00bb4d8025f5108178c8" +checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" dependencies = [ "async-trait", - "base64", + "base64 0.23.0", "chrono", "futures", "pastey", @@ -1709,9 +1715,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.0.0-beta.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5ce7541aff2f3df9e8c3c48c7241800fa6fa68e2fbcba386c0d7dcdfc953c" +checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1940,7 +1946,7 @@ version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", diff --git a/mcp-client-rust/Cargo.toml b/mcp-client-rust/Cargo.toml index 9699727b..4873b305 100644 --- a/mcp-client-rust/Cargo.toml +++ b/mcp-client-rust/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" anyhow = "1.0.100" genai = "0.4.2" jsonschema = "0.35" -rmcp = { version = "3.0.0-beta.2", features = ["server", "client", "transport-io", "transport-child-process"] } +rmcp = { version = "3.1", 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/weather-server-rust/Cargo.lock b/weather-server-rust/Cargo.lock index de61996e..9a2cd1f6 100644 --- a/weather-server-rust/Cargo.lock +++ b/weather-server-rust/Cargo.lock @@ -91,6 +91,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "bitflags" version = "2.9.1" @@ -517,7 +523,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1045,7 +1051,7 @@ version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1095,12 +1101,12 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.0.0-beta.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3875f7c471c2d709062bc2165d8dc883b021b44255bd00bb4d8025f5108178c8" +checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" dependencies = [ "async-trait", - "base64", + "base64 0.23.0", "chrono", "futures", "pastey", @@ -1118,9 +1124,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.0.0-beta.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5ce7541aff2f3df9e8c3c48c7241800fa6fa68e2fbcba386c0d7dcdfc953c" +checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" dependencies = [ "darling", "proc-macro2", diff --git a/weather-server-rust/Cargo.toml b/weather-server-rust/Cargo.toml index b97b6d15..bb1a3807 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 = "3.0.0-beta.2", features = ["server", "macros", "transport-io"] } +rmcp = { version = "3.1", 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/src/main.rs b/weather-server-rust/src/main.rs index 762e31bc..4447bf40 100644 --- a/weather-server-rust/src/main.rs +++ b/weather-server-rust/src/main.rs @@ -286,6 +286,9 @@ impl Weather { impl ServerHandler for Weather { fn get_info(&self) -> ServerInfo { let mut info = ServerInfo::default(); + // Without this the server reports rmcp's own crate name and version + // rather than its own. + info.server_info = Implementation::new("weather", "1.0.0"); info.capabilities = ServerCapabilities::builder().enable_tools().build(); info } From 506e1f70de855101b0ad0c058daa9ba95e27362a Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 1 Aug 2026 21:30:22 -0700 Subject: [PATCH 3/4] Say "structured content", not "structured output" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec never uses the phrase "structured output". It defines two separate things under Tool Result: "Structured Content" (the `structuredContent` field) and "Output Schema" (the `outputSchema` field). Collapsing them into "structured output" conflates the two. It is also actively confusing here. In LLM tooling "structured output" means constrained decoding — making the *model* emit conforming JSON. These clients call a model API, so a reader could reasonably take the phrase to mean the tool constrains the model's response, which is the opposite of what is going on: the tool describes the shape of its own result. Headings and prose now say "structured content". References to real identifiers are left alone: the Python SDK's own docs page is called Structured Output and lives at docs/servers/structured-output.md, and its decorator parameter is `structured_output`. Co-Authored-By: Claude Opus 5 --- mcp-client-rust/README.md | 2 +- weather-server-rust/README.md | 2 +- weather-server-rust/src/main.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mcp-client-rust/README.md b/mcp-client-rust/README.md index ad98e104..ef98b767 100644 --- a/mcp-client-rust/README.md +++ b/mcp-client-rust/README.md @@ -2,7 +2,7 @@ See the [Build an MCP client](https://modelcontextprotocol.io/docs/develop/build-client) tutorial for more information. -## Structured output +## Structured content 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. diff --git a/weather-server-rust/README.md b/weather-server-rust/README.md index a0d5bc61..9b9a5ca9 100644 --- a/weather-server-rust/README.md +++ b/weather-server-rust/README.md @@ -2,7 +2,7 @@ See the [Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) tutorial for more information. -## Structured output +## Structured content 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. diff --git a/weather-server-rust/src/main.rs b/weather-server-rust/src/main.rs index 4447bf40..23c0ff6a 100644 --- a/weather-server-rust/src/main.rs +++ b/weather-server-rust/src/main.rs @@ -68,7 +68,7 @@ struct ForecastPeriod { detailed_forecast: String, } -/// One element of `get_alerts`' structured output. Declaring `Vec` as +/// One element of `get_alerts`' structured content. 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)] @@ -85,7 +85,7 @@ pub struct Alert { instructions: String, } -/// `get_forecast`' structured output: the object case. +/// `get_forecast`' structured content: the object case. #[derive(Debug, Serialize, JsonSchema)] pub struct Forecast { /// Latitude the forecast is for. From ce055ae1fe832c53983fb7b2616d273f9cedfe7d Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 9 Aug 2026 17:52:24 -0700 Subject: [PATCH 4/4] Drop the hand-written list_tools now that rmcp emits cache hints `#[tool_handler]` generated `ttl_ms: None, cache_scope: None`, but both are required on a paginated result at 2026-07-28, so a strict client rejected tools/list before it could reach a tool call. The server worked around that by hand-writing list_tools purely to set the two fields. rust-sdk#1120 fixed the macro, released in rmcp 3.1.1: it now emits the fields when the negotiated revision expects them and omits them otherwise. Both lockfiles move to 3.1.2 and the override goes away, so the server is back to the plain `#[tool_handler]` path the SDK documents. Verified on the raw wire: after a 2026-07-28 handshake, tools/list carries `ttlMs: 0` and `cacheScope: "public"` with no code setting them, and the smoke test's strict client still validates both tools' structured content. --- mcp-client-rust/Cargo.lock | 48 +++++++++++++-------------------- weather-server-rust/Cargo.lock | 38 +++++++++----------------- weather-server-rust/README.md | 2 -- weather-server-rust/src/main.rs | 12 --------- 4 files changed, 31 insertions(+), 69 deletions(-) diff --git a/mcp-client-rust/Cargo.lock b/mcp-client-rust/Cargo.lock index 7b304cd0..bc923239 100644 --- a/mcp-client-rust/Cargo.lock +++ b/mcp-client-rust/Cargo.lock @@ -61,17 +61,6 @@ 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" @@ -218,12 +207,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.0", + "darling_macro 0.24.0", ] [[package]] @@ -242,15 +231,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 3.0.3", ] [[package]] @@ -266,13 +255,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.0", "quote", - "syn 2.0.106", + "syn 3.0.3", ] [[package]] @@ -1484,7 +1473,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1690,11 +1679,10 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" +checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8" dependencies = [ - "async-trait", "base64 0.23.0", "chrono", "futures", @@ -1715,15 +1703,15 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" +checksum = "6898e24cd16342b59bfa8a53c2c04b9cf62fc8a2cfea57b9c038b09984bfc521" dependencies = [ - "darling 0.23.0", + "darling 0.24.0", "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 3.0.3", ] [[package]] diff --git a/weather-server-rust/Cargo.lock b/weather-server-rust/Cargo.lock index 9a2cd1f6..f30eae56 100644 --- a/weather-server-rust/Cargo.lock +++ b/weather-server-rust/Cargo.lock @@ -47,17 +47,6 @@ 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" @@ -161,9 +150,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "darling" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" dependencies = [ "darling_core", "darling_macro", @@ -171,26 +160,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 3.0.3", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 3.0.3", ] [[package]] @@ -1101,11 +1090,10 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" +checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8" dependencies = [ - "async-trait", "base64 0.23.0", "chrono", "futures", @@ -1124,15 +1112,15 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" +checksum = "6898e24cd16342b59bfa8a53c2c04b9cf62fc8a2cfea57b9c038b09984bfc521" dependencies = [ "darling", "proc-macro2", "quote", "serde_json", - "syn 2.0.104", + "syn 3.0.3", ] [[package]] diff --git a/weather-server-rust/README.md b/weather-server-rust/README.md index 9b9a5ca9..647eec51 100644 --- a/weather-server-rust/README.md +++ b/weather-server-rust/README.md @@ -7,5 +7,3 @@ See the [Build an MCP server](https://modelcontextprotocol.io/docs/develop/build 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 23c0ff6a..230ae5e0 100644 --- a/weather-server-rust/src/main.rs +++ b/weather-server-rust/src/main.rs @@ -292,18 +292,6 @@ impl ServerHandler for Weather { 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]