diff --git a/.release-manifest.json b/.release-manifest.json
index e8ad288..db381e1 100644
--- a/.release-manifest.json
+++ b/.release-manifest.json
@@ -1,13 +1,15 @@
{
- "crates/rust-mcp-sdk": "0.5.1",
- "crates/rust-mcp-macros": "0.5.1",
- "crates/rust-mcp-transport": "0.4.1",
- "examples/hello-world-mcp-server": "0.1.25",
- "examples/hello-world-mcp-server-core": "0.1.16",
- "examples/simple-mcp-client": "0.1.25",
- "examples/simple-mcp-client-core": "0.1.25",
- "examples/hello-world-server-core-streamable-http": "0.1.16",
- "examples/hello-world-server-streamable-http": "0.1.25",
- "examples/simple-mcp-client-core-sse": "0.1.16",
- "examples/simple-mcp-client-sse": "0.1.16"
+ "crates/rust-mcp-sdk": "0.7.0",
+ "crates/rust-mcp-macros": "0.5.2",
+ "crates/rust-mcp-transport": "0.6.0",
+ "examples/hello-world-mcp-server-stdio": "0.1.29",
+ "examples/hello-world-mcp-server-stdio-core": "0.1.20",
+ "examples/simple-mcp-client-stdio": "0.1.29",
+ "examples/simple-mcp-client-stdio-core": "0.1.29",
+ "examples/hello-world-server-streamable-http-core": "0.1.20",
+ "examples/hello-world-server-streamable-http": "0.1.32",
+ "examples/simple-mcp-client-sse-core": "0.1.20",
+ "examples/simple-mcp-client-sse": "0.1.23",
+ "examples/simple-mcp-client-streamable-http": "0.1.1",
+ "examples/simple-mcp-client-streamable-http-core": "0.1.1"
}
diff --git a/Cargo.lock b/Cargo.lock
index df081df..0acb30d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,9 +61,9 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.88"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
@@ -84,9 +84,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
-version = "1.13.3"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
+checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -94,9 +94,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.30.0"
+version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff"
+checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd"
dependencies = [
"bindgen",
"cc",
@@ -118,7 +118,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"hyper-util",
"itoa",
"matchit",
@@ -170,7 +170,7 @@ dependencies = [
"fs-err",
"http 1.3.1",
"http-body 1.0.1",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"hyper-util",
"pin-project-lite",
"rustls",
@@ -216,32 +216,29 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
-version = "0.69.5"
+version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
- "lazy_static",
- "lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
- "rustc-hash 1.1.0",
+ "rustc-hash",
"shlex",
"syn",
- "which",
]
[[package]]
name = "bitflags"
-version = "2.9.1"
+version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bumpalo"
@@ -257,10 +254,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
-version = "1.2.32"
+version = "1.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
+checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
dependencies = [
+ "find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -277,9 +275,9 @@ dependencies = [
[[package]]
name = "cfg-if"
-version = "1.0.1"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
@@ -381,9 +379,9 @@ checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
[[package]]
name = "deranged"
-version = "0.4.0"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
]
@@ -426,16 +424,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-[[package]]
-name = "errno"
-version = "0.3.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
-dependencies = [
- "libc",
- "windows-sys 0.60.2",
-]
-
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -451,6 +439,12 @@ dependencies = [
"instant",
]
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -459,18 +453,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
-version = "3.1.1"
+version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683"
+checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd"
dependencies = [
"autocfg",
"tokio",
@@ -626,7 +620,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
@@ -687,8 +681,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]]
-name = "hello-world-mcp-server"
-version = "0.1.25"
+name = "hello-world-mcp-server-stdio"
+version = "0.1.29"
dependencies = [
"async-trait",
"futures",
@@ -701,8 +695,8 @@ dependencies = [
]
[[package]]
-name = "hello-world-mcp-server-core"
-version = "0.1.16"
+name = "hello-world-mcp-server-stdio-core"
+version = "0.1.20"
dependencies = [
"async-trait",
"futures",
@@ -713,8 +707,8 @@ dependencies = [
]
[[package]]
-name = "hello-world-server-core-streamable-http"
-version = "0.1.16"
+name = "hello-world-server-streamable-http"
+version = "0.1.32"
dependencies = [
"async-trait",
"futures",
@@ -727,8 +721,8 @@ dependencies = [
]
[[package]]
-name = "hello-world-server-streamable-http"
-version = "0.1.25"
+name = "hello-world-server-streamable-http-core"
+version = "0.1.20"
dependencies = [
"async-trait",
"futures",
@@ -746,15 +740,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
-[[package]]
-name = "home"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
-dependencies = [
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "http"
version = "0.2.12"
@@ -870,13 +855,14 @@ dependencies = [
[[package]]
name = "hyper"
-version = "1.6.0"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
dependencies = [
+ "atomic-waker",
"bytes",
"futures-channel",
- "futures-util",
+ "futures-core",
"h2 0.4.12",
"http 1.3.1",
"http-body 1.0.1",
@@ -884,6 +870,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
+ "pin-utils",
"smallvec",
"tokio",
"want",
@@ -896,7 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.3.1",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"hyper-util",
"rustls",
"rustls-pki-types",
@@ -908,9 +895,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.16"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
+checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -919,7 +906,7 @@ dependencies = [
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"ipnet",
"libc",
"percent-encoding",
@@ -1018,9 +1005,9 @@ dependencies = [
[[package]]
name = "idna"
-version = "1.0.3"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@@ -1039,9 +1026,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.10.0"
+version = "2.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
dependencies = [
"equivalent",
"hashbrown",
@@ -1064,9 +1051,9 @@ dependencies = [
[[package]]
name = "io-uring"
-version = "0.7.9"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
@@ -1091,9 +1078,9 @@ dependencies = [
[[package]]
name = "itertools"
-version = "0.12.1"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@@ -1106,9 +1093,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -1116,9 +1103,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.77"
+version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1130,12 +1117,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-[[package]]
-name = "lazycell"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
-
[[package]]
name = "libc"
version = "0.2.175"
@@ -1152,12 +1133,6 @@ dependencies = [
"windows-targets 0.53.3",
]
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
-
[[package]]
name = "litemap"
version = "0.8.0"
@@ -1182,9 +1157,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.27"
+version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru-slab"
@@ -1194,11 +1169,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
@@ -1267,12 +1242,11 @@ dependencies = [
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -1306,12 +1280,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
[[package]]
name = "parking"
version = "2.2.1"
@@ -1343,9 +1311,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
@@ -1361,9 +1329,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "potential_utf"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@@ -1385,9 +1353,9 @@ dependencies = [
[[package]]
name = "prettyplease"
-version = "0.2.36"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
@@ -1395,9 +1363,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.97"
+version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@@ -1420,19 +1388,19 @@ dependencies = [
[[package]]
name = "quinn"
-version = "0.11.8"
+version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"rustls",
- "socket2 0.5.10",
- "thiserror 2.0.14",
+ "socket2 0.6.0",
+ "thiserror 2.0.16",
"tokio",
"tracing",
"web-time",
@@ -1440,20 +1408,20 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.11.12"
+version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.2",
"ring",
- "rustc-hash 2.1.1",
+ "rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tinyvec",
"tracing",
"web-time",
@@ -1461,16 +1429,16 @@ dependencies = [
[[package]]
name = "quinn-udp"
-version = "0.5.13"
+version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2 0.6.0",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1569,47 +1537,32 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
name = "regex-automata"
-version = "0.4.9"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
[[package]]
name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.5"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "reqwest"
@@ -1626,7 +1579,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"hyper-rustls",
"hyper-util",
"js-sys",
@@ -1677,7 +1630,7 @@ dependencies = [
[[package]]
name = "rust-mcp-macros"
-version = "0.5.1"
+version = "0.5.2"
dependencies = [
"proc-macro2",
"quote",
@@ -1689,9 +1642,9 @@ dependencies = [
[[package]]
name = "rust-mcp-schema"
-version = "0.7.2"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0e71aee61257cd3d4a78fdc10c92c29e7a55c4f767119ffdafd837bb5e5cb9a"
+checksum = "0bb65fd293dbbfabaacba1512b3948cdd9bf31ad1f2c0fed4962052b590c5c44"
dependencies = [
"serde",
"serde_json",
@@ -1699,30 +1652,32 @@ dependencies = [
[[package]]
name = "rust-mcp-sdk"
-version = "0.5.1"
+version = "0.7.0"
dependencies = [
"async-trait",
"axum",
"axum-server",
+ "base64 0.22.1",
"futures",
- "hyper 1.6.0",
+ "hyper 1.7.0",
"reqwest",
"rust-mcp-macros",
"rust-mcp-schema",
"rust-mcp-transport",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
"uuid",
+ "wiremock",
]
[[package]]
name = "rust-mcp-transport"
-version = "0.4.1"
+version = "0.6.0"
dependencies = [
"async-trait",
"bytes",
@@ -1731,7 +1686,7 @@ dependencies = [
"rust-mcp-schema",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
"tokio-stream",
"tracing",
@@ -1744,31 +1699,12 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
-[[package]]
-name = "rustix"
-version = "0.38.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "rustls"
version = "0.23.31"
@@ -1805,9 +1741,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.103.4"
+version = "0.103.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
dependencies = [
"aws-lc-rs",
"ring",
@@ -1835,18 +1771,28 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.225"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.225"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
dependencies = [
"proc-macro2",
"quote",
@@ -1855,24 +1801,26 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.142"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
name = "serde_path_to_error"
-version = "0.1.17"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
+ "serde_core",
]
[[package]]
@@ -1923,8 +1871,24 @@ dependencies = [
]
[[package]]
-name = "simple-mcp-client"
-version = "0.1.25"
+name = "simple-mcp-client-sse"
+version = "0.1.23"
+dependencies = [
+ "async-trait",
+ "colored",
+ "futures",
+ "rust-mcp-sdk",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.16",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "simple-mcp-client-sse-core"
+version = "0.1.20"
dependencies = [
"async-trait",
"colored",
@@ -1932,13 +1896,15 @@ dependencies = [
"rust-mcp-sdk",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
+ "tracing",
+ "tracing-subscriber",
]
[[package]]
-name = "simple-mcp-client-core"
-version = "0.1.25"
+name = "simple-mcp-client-stdio"
+version = "0.1.29"
dependencies = [
"async-trait",
"colored",
@@ -1946,13 +1912,27 @@ dependencies = [
"rust-mcp-sdk",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
]
[[package]]
-name = "simple-mcp-client-core-sse"
-version = "0.1.16"
+name = "simple-mcp-client-stdio-core"
+version = "0.1.29"
+dependencies = [
+ "async-trait",
+ "colored",
+ "futures",
+ "rust-mcp-sdk",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.16",
+ "tokio",
+]
+
+[[package]]
+name = "simple-mcp-client-streamable-http"
+version = "0.1.1"
dependencies = [
"async-trait",
"colored",
@@ -1960,15 +1940,15 @@ dependencies = [
"rust-mcp-sdk",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
-name = "simple-mcp-client-sse"
-version = "0.1.16"
+name = "simple-mcp-client-streamable-http-core"
+version = "0.1.1"
dependencies = [
"async-trait",
"colored",
@@ -1976,7 +1956,7 @@ dependencies = [
"rust-mcp-sdk",
"serde",
"serde_json",
- "thiserror 2.0.14",
+ "thiserror 2.0.16",
"tokio",
"tracing",
"tracing-subscriber",
@@ -2028,9 +2008,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.104"
+version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -2068,11 +2048,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.14"
+version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
+checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
- "thiserror-impl 2.0.14",
+ "thiserror-impl 2.0.16",
]
[[package]]
@@ -2088,9 +2068,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.14"
+version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
+checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@@ -2108,12 +2088,11 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.41"
+version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
dependencies = [
"deranged",
- "itoa",
"num-conv",
"powerfmt",
"serde",
@@ -2123,15 +2102,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.4"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
-version = "0.2.22"
+version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
@@ -2149,9 +2128,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.9.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -2195,9 +2174,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.26.2"
+version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd"
dependencies = [
"rustls",
"tokio",
@@ -2319,14 +2298,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -2349,9 +2328,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
-version = "1.0.18"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "untrusted"
@@ -2361,9 +2340,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
-version = "2.5.4"
+version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -2379,9 +2358,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.18.0"
+version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
@@ -2429,30 +2408,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
-version = "0.14.2+wasi-0.2.4"
+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"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
- "wit-bindgen-rt",
+ "wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
+ "wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
dependencies = [
"bumpalo",
"log",
@@ -2464,9 +2453,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.50"
+version = "0.4.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
dependencies = [
"cfg-if",
"js-sys",
@@ -2477,9 +2466,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2487,9 +2476,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
dependencies = [
"proc-macro2",
"quote",
@@ -2500,9 +2489,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.100"
+version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
dependencies = [
"unicode-ident",
]
@@ -2522,9 +2511,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.77"
+version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2549,40 +2538,6 @@ dependencies = [
"rustls-pki-types",
]
-[[package]]
-name = "which"
-version = "4.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
-dependencies = [
- "either",
- "home",
- "once_cell",
- "rustix",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows-link"
version = "0.1.3"
@@ -2768,13 +2723,10 @@ dependencies = [
]
[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
+name = "wit-bindgen"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
-dependencies = [
- "bitflags",
-]
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
@@ -2808,18 +2760,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.26"
+version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.26"
+version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index a85b5a7..edb7e28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,21 +4,24 @@ members = [
"crates/rust-mcp-macros",
"crates/rust-mcp-sdk",
"crates/rust-mcp-transport",
- "examples/simple-mcp-client",
- "examples/simple-mcp-client-core",
- "examples/hello-world-mcp-server",
- "examples/hello-world-mcp-server-core",
+ "examples/simple-mcp-client-stdio",
+ "examples/simple-mcp-client-stdio-core",
+ "examples/hello-world-mcp-server-stdio",
+ "examples/hello-world-mcp-server-stdio-core",
"examples/hello-world-server-streamable-http",
- "examples/hello-world-server-core-streamable-http",
+ "examples/hello-world-server-streamable-http-core",
"examples/simple-mcp-client-sse",
- "examples/simple-mcp-client-core-sse",
+ "examples/simple-mcp-client-sse-core",
+ "examples/simple-mcp-client-streamable-http",
+ "examples/simple-mcp-client-streamable-http-core",
+
]
[workspace.dependencies]
# Workspace member crates
-rust-mcp-transport = { version = "0.4.1", path = "crates/rust-mcp-transport", default-features = false }
+rust-mcp-transport = { version = "0.6.0", path = "crates/rust-mcp-transport", default-features = false }
rust-mcp-sdk = { path = "crates/rust-mcp-sdk", default-features = false }
-rust-mcp-macros = { version = "0.5.1", path = "crates/rust-mcp-macros", default-features = false }
+rust-mcp-macros = { version = "0.5.2", path = "crates/rust-mcp-macros", default-features = false }
# External crates
rust-mcp-schema = { version = "0.7", default-features = false }
@@ -39,7 +42,7 @@ tracing-subscriber = { version = "0.3", features = [
"std",
"fmt",
] }
-
+base64 = "0.22"
axum = "0.8"
rustls = "0.23"
tokio-rustls = "0.26"
diff --git a/README.md b/README.md
index ef5b4ed..2c70c3e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[
](https://github.com/rust-mcp-stack/rust-mcp-sdk/actions/workflows/ci.yml)
[
-](examples/hello-world-mcp-server)
+](examples/hello-world-mcp-server-stdio)
A high-performance, asynchronous toolkit for building MCP servers and clients.
Focus on your app's logic while **rust-mcp-sdk** takes care of the rest!
@@ -32,15 +32,14 @@ This project supports following transports:
π The **rust-mcp-sdk** includes a lightweight [Axum](https://github.com/tokio-rs/axum) based server that handles all core functionality seamlessly. Switching between `stdio` and `Streamable HTTP` is straightforward, requiring minimal code changes. The server is designed to efficiently handle multiple concurrent client connections and offers built-in support for SSL.
-
**MCP Streamable HTTP Support**
- β
Streamable HTTP Support for MCP Servers
- β
DNS Rebinding Protection
- β
Batch Messages
- β
Streaming & non-streaming JSON response
-- β¬ Streamable HTTP Support for MCP Clients
-- β¬ Resumability
-- β¬ Authentication / Oauth
+- β
Streamable HTTP Support for MCP Clients
+- β
Resumability
+- β¬ Oauth Authentication
**β οΈ** Project is currently under development and should be used at your own risk.
@@ -49,7 +48,9 @@ This project supports following transports:
- [MCP Server (stdio)](#mcp-server-stdio)
- [MCP Server (Streamable HTTP)](#mcp-server-streamable-http)
- [MCP Client (stdio)](#mcp-client-stdio)
+ - [MCP Client (Streamable HTTP)](#mcp-client_streamable-http))
- [MCP Client (sse)](#mcp-client-sse)
+- [Macros](#macros)
- [Getting Started](#getting-started)
- [HyperServerOptions](#hyperserveroptions)
- [Security Considerations](#security-considerations)
@@ -110,7 +111,7 @@ async fn main() -> SdkResult<()> {
}
```
-See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
+See hello-world-mcp-server-stdio example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :

@@ -153,6 +154,7 @@ let server = hyper_server::create_server(
HyperServerOptions {
host: "127.0.0.1".to_string(),
sse_support: false,
+ event_store: Some(Arc::new(InMemoryEventStore::default())), // enable resumability
..Default::default()
},
);
@@ -180,7 +182,7 @@ pub struct MyServerHandler;
#[async_trait]
impl ServerHandler for MyServerHandler {
// Handle ListToolsRequest, return list of available tools as ListToolsResult
- async fn handle_list_tools_request(&self, request: ListToolsRequest, runtime: &dyn McpServer) -> Result {
+ async fn handle_list_tools_request(&self, request: ListToolsRequest, runtime: Arc) -> Result {
Ok(ListToolsResult {
tools: vec![SayHelloTool::tool()],
@@ -191,7 +193,7 @@ impl ServerHandler for MyServerHandler {
}
/// Handles requests to call a specific tool.
- async fn handle_call_tool_request( &self, request: CallToolRequest, runtime: &dyn McpServer, ) -> Result {
+ async fn handle_call_tool_request( &self, request: CallToolRequest, runtime: Arc ) -> Result {
if request.tool_name() == SayHelloTool::tool_name() {
Ok( CallToolResult::text_content( vec![TextContent::from("Hello World!".to_string())] ))
@@ -205,7 +207,7 @@ impl ServerHandler for MyServerHandler {
---
-π For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)**
+π For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)**
See hello-world-server-streamable-http example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
@@ -283,6 +285,8 @@ async fn main() -> SdkResult<()> {
println!("{}",result.content.first().unwrap().as_text_content()?.text);
+ client.shut_down().await?;
+
Ok(())
}
@@ -294,8 +298,82 @@ Here is the output :
> your results may vary slightly depending on the version of the MCP Server in use when you run it.
+### MCP Client (Streamable HTTP)
+```rs
+
+// STEP 1: Custom Handler to handle incoming MCP Messages
+pub struct MyClientHandler;
+
+#[async_trait]
+impl ClientHandler for MyClientHandler {
+ // To check out a list of all the methods in the trait that you can override, take a look at https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_client_handler.rs
+}
+
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+
+ // Step2 : Define client details and capabilities
+ let client_details: InitializeRequestParams = InitializeRequestParams {
+ capabilities: ClientCapabilities::default(),
+ client_info: Implementation {
+ name: "simple-rust-mcp-client-sse".to_string(),
+ version: "0.1.0".to_string(),
+ title: Some("Simple Rust MCP Client (SSE)".to_string()),
+ },
+ protocol_version: LATEST_PROTOCOL_VERSION.into(),
+ };
+
+ // Step 3: Create transport options to connect to an MCP server via Streamable HTTP.
+ let transport_options = StreamableTransportOptions {
+ mcp_url: MCP_SERVER_URL.to_string(),
+ request_options: RequestOptions {
+ ..RequestOptions::default()
+ },
+ };
+
+ // STEP 4: instantiate the custom handler that is responsible for handling MCP messages
+ let handler = MyClientHandler {};
+
+ // STEP 5: create the client with transport options and the handler
+ let client = client_runtime::with_transport_options(client_details, transport_options, handler);
+
+ // STEP 6: start the MCP client
+ client.clone().start().await?;
+
+ // STEP 7: use client methods to communicate with the MCP Server as you wish
+
+ // Retrieve and display the list of tools available on the server
+ let server_version = client.server_version().unwrap();
+ let tools = client.list_tools(None).await?.tools;
+ println!("List of tools for {}@{}", server_version.name, server_version.version);
+
+ tools.iter().enumerate().for_each(|(tool_index, tool)| {
+ println!(" {}. {} : {}",
+ tool_index + 1,
+ tool.name,
+ tool.description.clone().unwrap_or_default()
+ );
+ });
+
+ println!("Call \"add\" tool with 100 and 28 ...");
+ // Create a `Map` to represent the tool parameters
+ let params = json!({"a": 100,"b": 28}).as_object().unwrap().clone();
+ let request = CallToolRequestParams { name: "add".to_string(),arguments: Some(params)};
+
+ // invoke the tool
+ let result = client.call_tool(request).await?;
+
+ println!("{}",result.content.first().unwrap().as_text_content()?.text);
+
+ client.shut_down().await?;
+
+ Ok(())
+```
+π see [examples/simple-mcp-client-streamable-http](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-streamable-http) for a complete working example.
+
+
### MCP Client (sse)
-Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical, with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same:
+Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical to the [stdio example](#mcp-client-stdio) , with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same:
```diff
- let transport = StdioTransport::create_with_server_launch(
@@ -306,6 +384,116 @@ Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost
+ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?;
```
+π see [examples/simple-mcp-client-sse](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-sse) for a complete working example.
+
+
+## Macros
+[rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) includes several helpful macros that simplify common tasks when building MCP servers and clients. For example, they can automatically generate tool specifications and tool schemas right from your structs, or assist with elicitation requests and responses making them completely type safe.
+
+> To use these macros, ensure the `macros` feature is enabled in your Cargo.toml.
+
+### mcp_tool
+`mcp_tool` is a procedural macro attribute that helps generating rust_mcp_schema::Tool from a struct.
+
+Usage example:
+```rust
+#[mcp_tool(
+ name = "move_file",
+ title="Move File",
+ description = concat!("Move or rename files and directories. Can move files between directories ",
+"and rename them in a single operation. If the destination exists, the ",
+"operation will fail. Works across different directories and can be used ",
+"for simple renaming within the same directory. ",
+"Both source and destination must be within allowed directories."),
+ destructive_hint = false,
+ idempotent_hint = false,
+ open_world_hint = false,
+ read_only_hint = false
+)]
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, JsonSchema)]
+pub struct MoveFileTool {
+ /// The source path of the file to move.
+ pub source: String,
+ /// The destination path to move the file to.
+ pub destination: String,
+}
+
+// Now we can call `tool()` method on it to get a Tool instance
+let rust_mcp_sdk::schema::Tool = MoveFileTool::tool();
+
+```
+
+π» For a real-world example, check out any of the tools available at: https://github.com/rust-mcp-stack/rust-mcp-filesystem/tree/main/src/tools
+
+
+### tool_box
+`tool_box` generates an enum from a provided list of tools, making it easier to organize and manage them, especially when your application includes a large number of tools.
+
+It accepts an array of tools and generates an enum where each tool becomes a variant of the enum.
+
+Generated enum has a `tools()` function that returns a `Vec` , and a `TryFrom` trait implementation that could be used to convert a ToolRequest into a Tool instance.
+
+Usage example:
+```rust
+ // Accepts an array of tools and generates an enum named `FileSystemTools`,
+ // where each tool becomes a variant of the enum.
+ tool_box!(FileSystemTools, [ReadFileTool, MoveFileTool, SearchFilesTool]);
+
+ // now in the app, we can use the FileSystemTools, like:
+ let all_tools: Vec = FileSystemTools::tools();
+```
+
+π» To see a real-world example of that please see :
+- `tool_box` macro usage: [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/tools.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/tools.rs)
+- using `tools()` in list tools request : [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L67)
+- using `try_from` in call tool_request: [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L100)
+
+
+
+### mcp_elicit
+The `mcp_elicit` macro generates implementations for the annotated struct to facilitate data elicitation. It enables struct to generate `ElicitRequestedSchema` and also parsing a map of field names to `ElicitResultContentValue` values back into the struct, supporting both required and optional fields. The generated implementation includes:
+
+- A `message()` method returning the elicitation message as a string.
+- A `requested_schema()` method returning an `ElicitRequestedSchema` based on the structβs JSON schema.
+- A `from_content_map()` method to convert a map of `ElicitResultContentValue` values into a struct instance.
+
+### Attributes
+
+- `message` - An optional string (or `concat!(...)` expression) to prompt the user or system for input. Defaults to an empty string if not provided.
+
+Usage example:
+```rust
+// A struct that could be used to send elicit request and get the input from the user
+#[mcp_elicit(message = "Please enter your info")]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(
+ title = "Name",
+ description = "The user's full name",
+ min_length = 5,
+ max_length = 100
+ )]
+ pub name: String,
+ /// Is user a student?
+ #[json_schema(title = "Is student?", default = true)]
+ pub is_student: Option,
+
+ /// User's favorite color
+ pub favorate_color: Colors,
+}
+
+// send a Elicit Request , ask for UserInfo data and convert the result back to a valid UserInfo instance
+let result: ElicitResult = server
+ .elicit_input(UserInfo::message(), UserInfo::requested_schema())
+ .await?;
+
+// Create a UserInfo instance using data provided by the user on the client side
+let user_info = UserInfo::from_content_map(result.content)?;
+
+```
+
+π» For mre info please see :
+- https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/crates/rust-mcp-macros
## Getting Started
@@ -337,6 +525,7 @@ server.start().await?;
Here is a list of available options with descriptions for configuring the HyperServer:
```rs
+
pub struct HyperServerOptions {
/// Hostname or IP address the server will bind to (default: "127.0.0.1")
pub host: String,
@@ -344,9 +533,19 @@ pub struct HyperServerOptions {
/// Hostname or IP address the server will bind to (default: "8080")
pub port: u16,
+ /// Optional thread-safe session id generator to generate unique session IDs.
+ pub session_id_generator: Option>>,
+
/// Optional custom path for the Streamable HTTP endpoint (default: `/mcp`)
pub custom_streamable_http_endpoint: Option,
+ /// Shared transport configuration used by the server
+ pub transport_options: Arc,
+
+ /// Event store for resumability support
+ /// If provided, resumability will be enabled, allowing clients to reconnect and resume messages
+ pub event_store: Option>,
+
/// This setting only applies to streamable HTTP.
/// If true, the server will return JSON responses instead of starting an SSE stream.
/// This can be useful for simple request/response scenarios without streaming.
@@ -356,12 +555,6 @@ pub struct HyperServerOptions {
/// Interval between automatic ping messages sent to clients to detect disconnects
pub ping_interval: Duration,
- /// Shared transport configuration used by the server
- pub transport_options: Arc,
-
- /// Optional thread-safe session id generator to generate unique session IDs.
- pub session_id_generator: Option>,
-
/// Enables SSL/TLS if set to `true`
pub enable_ssl: bool,
@@ -373,17 +566,6 @@ pub struct HyperServerOptions {
/// Required if `enable_ssl` is `true`.
pub ssl_key_path: Option,
- /// If set to true, the SSE transport will also be supported for backward compatibility (default: true)
- pub sse_support: bool,
-
- /// Optional custom path for the Server-Sent Events (SSE) endpoint (default: `/sse`)
- /// Applicable only if sse_support is true
- pub custom_sse_endpoint: Option,
-
- /// Optional custom path for the MCP messages endpoint for sse (default: `/messages`)
- /// Applicable only if sse_support is true
- pub custom_messages_endpoint: Option,
-
/// List of allowed host header values for DNS rebinding protection.
/// If not specified, host validation is disabled.
pub allowed_hosts: Option>,
@@ -395,6 +577,17 @@ pub struct HyperServerOptions {
/// Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured).
/// Default is false for backwards compatibility.
pub dns_rebinding_protection: bool,
+
+ /// If set to true, the SSE transport will also be supported for backward compatibility (default: true)
+ pub sse_support: bool,
+
+ /// Optional custom path for the Server-Sent Events (SSE) endpoint (default: `/sse`)
+ /// Applicable only if sse_support is true
+ pub custom_sse_endpoint: Option,
+
+ /// Optional custom path for the MCP messages endpoint for sse (default: `/messages`)
+ /// Applicable only if sse_support is true
+ pub custom_messages_endpoint: Option,
}
```
@@ -416,9 +609,15 @@ The `rust-mcp-sdk` crate provides several features that can be enabled or disabl
- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP servers.
- `client`: Activates MCP client capabilities, offering modules and APIs for client development and communicating with MCP servers.
-- `hyper-server`: This feature enables the **sse** transport for MCP servers, supporting multiple simultaneous client connections out of the box.
-- `ssl`: This feature enables TLS/SSL support for the **sse** transport when used with the `hyper-server`.
+- `hyper-server`: This feature is necessary to enable `Streamable HTTP` or `Server-Sent Events (SSE)` transports for MCP servers. It must be used alongside the server feature to support the required server functionalities.
+- `ssl`: This feature enables TLS/SSL support for the `Streamable HTTP` or `Server-Sent Events (SSE)` transport when used with the `hyper-server`.
- `macros`: Provides procedural macros for simplifying the creation and manipulation of MCP Tool structures.
+- `sse`: Enables support for the `Server-Sent Events (SSE)` transport.
+- `streamable-http`: Enables support for the `Streamable HTTP` transport.
+
+- `stdio`: Enables support for the `standard input/output (stdio)` transport.
+- `tls-no-provider`: Enables TLS without a crypto provider. This is useful if you are already using a different crypto provider than the aws-lc default.
+
#### MCP Protocol Versions with Corresponding Features
@@ -449,9 +648,9 @@ If you only need the MCP Server functionality, you can disable the default featu
```toml
[dependencies]
-rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros"] }
+rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros","stdio"] }
```
-Optionally add `hyper-server` for **sse** transport, and `ssl` feature for tls/ssl support of the `hyper-server`
+Optionally add `hyper-server` and `streamable-http` for **Streamable HTTP** transport, and `ssl` feature for tls/ssl support of the `hyper-server`
@@ -464,7 +663,7 @@ Add the following to your Cargo.toml:
```toml
[dependencies]
-rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["client","2024_11_05"] }
+rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["client","2024_11_05","stdio"] }
```
@@ -477,10 +676,10 @@ Learn when to use the `mcp_*_handler` traits versus the lower-level `mcp_*_hand
[rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) provides two type of handler traits that you can chose from:
- **ServerHandler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs.
- Refer to [examples/hello-world-mcp-server/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server/src/handler.rs) for an example.
+ Refer to [examples/hello-world-mcp-server-stdio/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio/src/handler.rs) for an example.
- **ServerHandlerCore**: If you need more control over MCP messages, consider using `ServerHandlerCore`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters.
- Refer to [examples/hello-world-mcp-server-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-core/src/handler.rs) for an example.
+ Refer to [examples/hello-world-mcp-server-stdio-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio-core/src/handler.rs) for an example.
---
@@ -509,7 +708,7 @@ Both functions create an MCP client instance.
-Check out the corresponding examples at: [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) and [examples/simple-mcp-client-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-core).
+Check out the corresponding examples at: [examples/simple-mcp-client-stdio](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-stdio) and [examples/simple-mcp-client-stdio-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-stdio-core).
## Projects using Rust MCP SDK
@@ -526,6 +725,11 @@ Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their na
|
| [text-to-cypher](https://github.com/FalkorDB/text-to-cypher) | A high-performance Rust-based API service that translates natural language text to Cypher queries for graph databases. | [GitHub](https://github.com/FalkorDB/text-to-cypher) |
|
| [notify-mcp](https://github.com/Tuurlijk/notify-mcp) | A Model Context Protocol (MCP) server that provides desktop notification functionality. | [GitHub](https://github.com/Tuurlijk/notify-mcp) |
|
| [lst](https://github.com/WismutHansen/lst) | `lst` is a personal lists, notes, and blog posts management application with a focus on plain-text storage, offline-first functionality, and multi-device synchronization. | [GitHub](https://github.com/WismutHansen/lst) |
+|
| [rust-mcp-server](https://github.com/Vaiz/rust-mcp-server) | `rust-mcp-server` allows the model to perform actions on your behalf, such as building, testing, and analyzing your Rust code. | [GitHub](https://github.com/Vaiz/rust-mcp-server) |
+
+
+
+
diff --git a/crates/rust-mcp-macros/CHANGELOG.md b/crates/rust-mcp-macros/CHANGELOG.md
index a7b5306..69b3059 100644
--- a/crates/rust-mcp-macros/CHANGELOG.md
+++ b/crates/rust-mcp-macros/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.5.2](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-macros-v0.5.1...rust-mcp-macros-v0.5.2) (2025-09-19)
+
+
+### π Features
+
+* Add elicitation macros and add elicit_input() method ([#99](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/99)) ([3ab5fe7](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/3ab5fe73aaa10de2b5b23caee357ac15b37c845f))
+
## [0.5.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-macros-v0.5.0...rust-mcp-macros-v0.5.1) (2025-08-12)
diff --git a/crates/rust-mcp-macros/Cargo.toml b/crates/rust-mcp-macros/Cargo.toml
index 0dfdc56..9c2dd5a 100644
--- a/crates/rust-mcp-macros/Cargo.toml
+++ b/crates/rust-mcp-macros/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rust-mcp-macros"
-version = "0.5.1"
+version = "0.5.2"
authors = ["Ali Hashemi"]
categories = ["data-structures", "parser-implementations", "parsing"]
description = "A procedural macro that derives the MCPToolSchema implementation for structs or enums, generating a tool_input_schema function used with rust_mcp_schema::Tool."
diff --git a/crates/rust-mcp-macros/README.md b/crates/rust-mcp-macros/README.md
index 92da2c3..fc463cd 100644
--- a/crates/rust-mcp-macros/README.md
+++ b/crates/rust-mcp-macros/README.md
@@ -1,5 +1,8 @@
# rust-mcp-macros.
+
+## mcp_tool Macro
+
A procedural macro, part of the [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) ecosystem, to generate `rust_mcp_schema::Tool` instance from a struct.
The `mcp_tool` macro generates an implementation for the annotated struct that includes:
@@ -80,11 +83,7 @@ fn main() {
```
----
-
Check out [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) , a high-performance, asynchronous toolkit for building MCP servers and clients. Focus on your app's logic while [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) takes care of the rest!
-
----
**Note**: The following attributes are available only in version `2025_03_26` and later of the MCP Schema, and their values will be used in the [annotations](https://github.com/rust-mcp-stack/rust-mcp-schema/blob/main/src/generated_schema/2025_03_26/mcp_schema.rs#L5557) attribute of the *[Tool struct](https://github.com/rust-mcp-stack/rust-mcp-schema/blob/main/src/generated_schema/2025_03_26/mcp_schema.rs#L5554-L5566).
@@ -93,3 +92,106 @@ fn main() {
- `idempotent_hint`
- `open_world_hint`
- `read_only_hint`
+
+
+
+
+
+## mcp_elicit Macro
+
+The `mcp_elicit` macro generates implementations for the annotated struct to facilitate data elicitation. It enables struct to generate `ElicitRequestedSchema` and also parsing a map of field names to `ElicitResultContentValue` values back into the struct, supporting both required and optional fields. The generated implementation includes:
+
+- A `message()` method returning the elicitation message as a string.
+- A `requested_schema()` method returning an `ElicitRequestedSchema` based on the structβs JSON schema.
+- A `from_content_map()` method to convert a map of `ElicitResultContentValue` values into a struct instance.
+
+### Attributes
+
+- `message` - An optional string (or `concat!(...)` expression) to prompt the user or system for input. Defaults to an empty string if not provided.
+
+### Supported Field Types
+
+- `String`: Maps to `ElicitResultContentValue::String`.
+- `bool`: Maps to `ElicitResultContentValue::Boolean`.
+- `i32`: Maps to `ElicitResultContentValue::Integer` (with bounds checking).
+- `i64`: Maps to `ElicitResultContentValue::Integer`.
+- `enum` Only simple enums are supported. The enum must implement the FromStr trait.
+- `Option`: Supported for any of the above types, mapping to `None` if the field is missing.
+
+
+### Usage Example
+
+```rust
+use rust_mcp_sdk::macros::{mcp_elicit, JsonSchema};
+use rust_mcp_sdk::schema::RpcError;
+use std::str::FromStr;
+
+// Simple enum with FromStr trait implemented
+#[derive(JsonSchema, Debug)]
+pub enum Colors {
+ #[json_schema(title = "Green Color")]
+ Green,
+ #[json_schema(title = "Red Color")]
+ Red,
+}
+impl FromStr for Colors {
+ type Err = RpcError;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "green" => Ok(Colors::Green),
+ "red" => Ok(Colors::Red),
+ _ => Err(RpcError::parse_error().with_message("Invalid color".to_string())),
+ }
+ }
+}
+
+// A struct that could be used to send elicit request and get the input from the user
+#[mcp_elicit(message = "Please enter your info")]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(
+ title = "Name",
+ description = "The user's full name",
+ min_length = 5,
+ max_length = 100
+ )]
+ pub name: String,
+
+ /// Email address of the user
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+
+ /// The user's age in years
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+
+ /// Is user a student?
+ #[json_schema(title = "Is student?", default = true)]
+ pub is_student: Option,
+
+ /// User's favorite color
+ pub favorate_color: Colors,
+}
+
+ // ....
+ // .......
+ // ...........
+
+ // send a Elicit Request , ask for UserInfo data and convert the result back to a valid UserInfo instance
+
+ let result: ElicitResult = server
+ .elicit_input(UserInfo::message(), UserInfo::requested_schema())
+ .await?;
+
+ // Create a UserInfo instance using data provided by the user on the client side
+ let user_info = UserInfo::from_content_map(result.content)?;
+
+
+```
+
+---
+
+
Check out [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk), a high-performance, asynchronous toolkit for building MCP servers and clients. Focus on your app's logic while [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) takes care of the rest!
+
+---
diff --git a/crates/rust-mcp-macros/src/lib.rs b/crates/rust-mcp-macros/src/lib.rs
index 35d6e55..473792c 100644
--- a/crates/rust-mcp-macros/src/lib.rs
+++ b/crates/rust-mcp-macros/src/lib.rs
@@ -6,7 +6,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::Parse, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Error, Expr,
- ExprLit, Fields, Lit, Meta, Token,
+ ExprLit, Fields, GenericArgument, Lit, Meta, PathArguments, Token, Type,
};
use utils::{is_option, renamed_field, type_to_json_schema};
@@ -45,6 +45,8 @@ struct McpToolMacroAttributes {
use syn::parse::ParseStream;
+use crate::utils::{generate_enum_parse, is_enum};
+
struct ExprList {
exprs: Punctuated,
}
@@ -246,6 +248,66 @@ impl Parse for McpToolMacroAttributes {
}
}
+struct McpElicitationAttributes {
+ message: Option,
+}
+
+impl Parse for McpElicitationAttributes {
+ fn parse(attributes: syn::parse::ParseStream) -> syn::Result {
+ let mut instance = Self { message: None };
+ let meta_list: Punctuated = Punctuated::parse_terminated(attributes)?;
+ for meta in meta_list {
+ if let Meta::NameValue(meta_name_value) = meta {
+ let ident = meta_name_value.path.get_ident().unwrap();
+ let ident_str = ident.to_string();
+ if ident_str.as_str() == "message" {
+ let value = match &meta_name_value.value {
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) => lit_str.value(),
+ Expr::Macro(expr_macro) => {
+ let mac = &expr_macro.mac;
+ if mac.path.is_ident("concat") {
+ let args: ExprList = syn::parse2(mac.tokens.clone())?;
+ let mut result = String::new();
+ for expr in args.exprs {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) = expr
+ {
+ result.push_str(&lit_str.value());
+ } else {
+ return Err(Error::new_spanned(
+ expr,
+ "Only string literals are allowed inside concat!()",
+ ));
+ }
+ }
+ result
+ } else {
+ return Err(Error::new_spanned(
+ expr_macro,
+ "Only concat!(...) is supported here",
+ ));
+ }
+ }
+ _ => {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected a string literal or concat!(...)",
+ ));
+ }
+ };
+ instance.message = Some(value)
+ }
+ }
+ }
+ Ok(instance)
+ }
+}
+
/// A procedural macro attribute to generate rust_mcp_schema::Tool related utility methods for a struct.
///
/// The `mcp_tool` macro generates an implementation for the annotated struct that includes:
@@ -387,7 +449,7 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
let output = quote! {
impl #input_ident {
- /// Returns the name of the tool as a string.
+ /// Returns the name of the tool as a String.
pub fn tool_name() -> String {
#tool_name.to_string()
}
@@ -404,7 +466,7 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
.iter()
.filter_map(|item| item.as_str().map(String::from))
.collect(),
- None => Vec::new(), // Default to an empty vector if "required" is missing or not an array
+ None => Vec::new(),
};
let properties: Option<
@@ -440,6 +502,303 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
TokenStream::from(output)
}
+#[proc_macro_attribute]
+pub fn mcp_elicit(attributes: TokenStream, input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let input_ident = &input.ident;
+
+ // Conditionally select the path
+ let base_crate = if cfg!(feature = "sdk") {
+ quote! { rust_mcp_sdk::schema }
+ } else {
+ quote! { rust_mcp_schema }
+ };
+
+ let macro_attributes = parse_macro_input!(attributes as McpElicitationAttributes);
+ let message = macro_attributes.message.unwrap_or_default();
+
+ // Generate field assignments for from_content_map()
+ let field_assignments = match &input.data {
+ Data::Struct(data) => match &data.fields {
+ Fields::Named(fields) => {
+ let assignments = fields.named.iter().map(|field| {
+ let field_attrs = &field.attrs;
+ let field_ident = &field.ident;
+ let renamed_field = renamed_field(field_attrs);
+ let field_name = renamed_field.unwrap_or_else(|| field_ident.as_ref().unwrap().to_string());
+ let field_type = &field.ty;
+
+ let type_check = if is_option(field_type) {
+ // Extract inner type for Option
+ let inner_type = match field_type {
+ Type::Path(type_path) => {
+ let segment = type_path.path.segments.last().unwrap();
+ if segment.ident == "Option" {
+ match &segment.arguments {
+ PathArguments::AngleBracketed(args) => {
+ match args.args.first().unwrap() {
+ GenericArgument::Type(ty) => ty,
+ _ => panic!("Expected type argument in Option"),
+ }
+ }
+ _ => panic!("Invalid Option type"),
+ }
+ } else {
+ panic!("Expected Option type");
+ }
+ }
+ _ => panic!("Expected Option type"),
+ };
+ // Determine the match arm based on the inner type at compile time
+ let (inner_type_ident, match_pattern, conversion) = match inner_type {
+ Type::Path(type_path) if type_path.path.is_ident("String") => (
+ quote! { String },
+ quote! { #base_crate::ElicitResultContentValue::String(s) },
+ quote! { s.clone() }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("bool") => (
+ quote! { bool },
+ quote! { #base_crate::ElicitResultContentValue::Boolean(b) },
+ quote! { *b }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("i32") => (
+ quote! { i32 },
+ quote! { #base_crate::ElicitResultContentValue::Integer(i) },
+ quote! {
+ (*i).try_into().map_err(|_| #base_crate::RpcError::parse_error().with_message(format!(
+ "Invalid number for field '{}': value {} does not fit in i32",
+ #field_name, *i
+ )))?
+ }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("i64") => (
+ quote! { i64 },
+ quote! { #base_crate::ElicitResultContentValue::Integer(i) },
+ quote! { *i }
+ ),
+ _ if is_enum(inner_type, &input) => {
+ let enum_parse = generate_enum_parse(inner_type, &field_name, &base_crate);
+ (
+ quote! { #inner_type },
+ quote! { #base_crate::ElicitResultContentValue::String(s) },
+ quote! { #enum_parse }
+ )
+ }
+ _ => panic!("Unsupported inner type for Option field: {}", quote! { #inner_type }),
+ };
+ let inner_type_str = quote! { stringify!(#inner_type_ident) };
+ quote! {
+ let #field_ident: Option<#inner_type_ident> = match content.as_ref().and_then(|map| map.get(#field_name)) {
+ Some(value) => {
+ match value {
+ #match_pattern => Some(#conversion),
+ _ => {
+ return Err(#base_crate::RpcError::parse_error().with_message(format!(
+ "Type mismatch for field '{}': expected {}, found {}",
+ #field_name, #inner_type_str,
+ match value {
+ #base_crate::ElicitResultContentValue::Boolean(_) => "boolean",
+ #base_crate::ElicitResultContentValue::String(_) => "string",
+ #base_crate::ElicitResultContentValue::Integer(_) => "integer",
+ }
+ )));
+ }
+ }
+ }
+ None => None,
+ };
+ }
+ } else {
+ // Determine the match arm based on the field type at compile time
+ let (field_type_ident, match_pattern, conversion) = match field_type {
+ Type::Path(type_path) if type_path.path.is_ident("String") => (
+ quote! { String },
+ quote! { #base_crate::ElicitResultContentValue::String(s) },
+ quote! { s.clone() }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("bool") => (
+ quote! { bool },
+ quote! { #base_crate::ElicitResultContentValue::Boolean(b) },
+ quote! { *b }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("i32") => (
+ quote! { i32 },
+ quote! { #base_crate::ElicitResultContentValue::Integer(i) },
+ quote! {
+ (*i).try_into().map_err(|_| #base_crate::RpcError::parse_error().with_message(format!(
+ "Invalid number for field '{}': value {} does not fit in i32",
+ #field_name, *i
+ )))?
+ }
+ ),
+ Type::Path(type_path) if type_path.path.is_ident("i64") => (
+ quote! { i64 },
+ quote! { #base_crate::ElicitResultContentValue::Integer(i) },
+ quote! { *i }
+ ),
+ _ if is_enum(field_type, &input) => {
+ let enum_parse = generate_enum_parse(field_type, &field_name, &base_crate);
+ (
+ quote! { #field_type },
+ quote! { #base_crate::ElicitResultContentValue::String(s) },
+ quote! { #enum_parse }
+ )
+ }
+ _ => panic!("Unsupported field type: {}", quote! { #field_type }),
+ };
+ let type_str = quote! { stringify!(#field_type_ident) };
+ quote! {
+ let #field_ident: #field_type_ident = match content.as_ref().and_then(|map| map.get(#field_name)) {
+ Some(value) => {
+ match value {
+ #match_pattern => #conversion,
+ _ => {
+ return Err(#base_crate::RpcError::parse_error().with_message(format!(
+ "Type mismatch for field '{}': expected {}, found {}",
+ #field_name, #type_str,
+ match value {
+ #base_crate::ElicitResultContentValue::Boolean(_) => "boolean",
+ #base_crate::ElicitResultContentValue::String(_) => "string",
+ #base_crate::ElicitResultContentValue::Integer(_) => "integer",
+ }
+ )));
+ }
+ }
+ }
+ None => {
+ return Err(#base_crate::RpcError::parse_error().with_message(format!(
+ "Missing required field: {}",
+ #field_name
+ )));
+ }
+ };
+ }
+ };
+
+ type_check
+ });
+
+ let field_idents = fields.named.iter().map(|field| &field.ident);
+
+ quote! {
+ #(#assignments)*
+
+ Ok(Self {
+ #(#field_idents,)*
+ })
+ }
+ }
+ _ => panic!("mcp_elicit macro only supports structs with named fields"),
+ },
+ _ => panic!("mcp_elicit macro only supports structs"),
+ };
+
+ let output = quote! {
+ impl #input_ident {
+
+ /// Returns the elicitation message defined in the `#[mcp_elicit(message = "...")]` attribute.
+ ///
+ /// This message is used to prompt the user or system for input when eliciting data for the struct.
+ /// If no message is provided in the attribute, an empty string is returned.
+ ///
+ /// # Returns
+ /// A `String` containing the elicitation message.
+ pub fn message()->String{
+ #message.to_string()
+ }
+
+ /// This method returns a `ElicitRequestedSchema` by retrieves the
+ /// struct's JSON schema (via the `JsonSchema` derive) and converting int into
+ /// a `ElicitRequestedSchema`. It extracts the `required` fields and
+ /// `properties` from the schema, mapping them to a `HashMap` of `PrimitiveSchemaDefinition` objects.
+ ///
+ /// # Returns
+ /// An `ElicitRequestedSchema` representing the schema of the struct.
+ ///
+ /// # Panics
+ /// Panics if the schema's properties cannot be converted to `PrimitiveSchemaDefinition` or if the schema
+ /// is malformed.
+ pub fn requested_schema() -> #base_crate::ElicitRequestedSchema {
+ let json_schema = input_ident::json_schema();
+
+ let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
+ Some(arr) => arr
+ .iter()
+ .filter_map(|item| item.as_str().map(String::from))
+ .collect(),
+ None => Vec::new(),
+ };
+
+ let properties: Option> = json_schema
+ .get("properties")
+ .and_then(|v| v.as_object()) // Safely extract "properties" as an object.
+ .map(|properties| {
+ properties
+ .iter()
+ .filter_map(|(key, value)| {
+ serde_json::to_value(value)
+ .ok() // If serialization fails, return None.
+ .and_then(|v| {
+ if let serde_json::Value::Object(obj) = v {
+ Some(obj)
+ } else {
+ None
+ }
+ })
+ .map(|obj| (key.to_string(), #base_crate::PrimitiveSchemaDefinition::try_from(&obj)))
+ })
+ .collect()
+ });
+
+ let properties = properties
+ .map(|map| {
+ map.into_iter()
+ .map(|(k, v)| v.map(|ok_v| (k, ok_v))) // flip Result inside tuple
+ .collect::, _>>() // collect only if all Ok
+ })
+ .transpose()
+ .unwrap();
+
+ let properties =
+ properties.expect("Was not able to create a ElicitRequestedSchema");
+
+ let requested_schema = #base_crate::ElicitRequestedSchema::new(properties, required);
+ requested_schema
+ }
+
+ /// Converts a map of field names and `ElicitResultContentValue` into an instance of the struct.
+ ///
+ /// This method parses the provided content map, matching field names to struct fields and converting
+ /// `ElicitResultContentValue` variants into the appropriate Rust types (e.g., `String`, `bool`, `i32`,
+ /// `i64`, or simple enums). It supports both required and optional fields (`Option`).
+ ///
+ /// # Parameters
+ /// - `content`: An optional `HashMap` mapping field names to `ElicitResultContentValue` values.
+ ///
+ /// # Returns
+ /// - `Ok(Self)` if the map is successfully parsed into the struct.
+ /// - `Err(RpcError)` if:
+ /// - A required field is missing.
+ /// - A valueβs type does not match the expected field type.
+ /// - An integer value cannot be converted (e.g., `i64` to `i32` out of bounds).
+ /// - An enum value is invalid (e.g., string value does not match a enum variant name).
+ ///
+ /// # Errors
+ /// Returns `RpcError` with messages like:
+ /// - `"Missing required field: {}"`
+ /// - `"Type mismatch for field '{}': expected {}, found {}"`
+ /// - `"Invalid number for field '{}': value {} does not fit in i32"`
+ /// - `"Invalid enum value for field '{}': expected 'Yes' or 'No', found '{}'"`.
+ pub fn from_content_map(content: ::std::option::Option<::std::collections::HashMap<::std::string::String, #base_crate::ElicitResultContentValue>>) -> Result {
+ #field_assignments
+ }
+ }
+ #input
+ };
+
+ TokenStream::from(output)
+}
+
/// Derives a JSON Schema representation for a struct.
///
/// This procedural macro generates a `json_schema()` method for the annotated struct, returning a
@@ -473,70 +832,222 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
/// # Dependencies
/// Relies on `serde_json` for `Map` and `Value` types.
///
-#[proc_macro_derive(JsonSchema)]
+#[proc_macro_derive(JsonSchema, attributes(json_schema))]
pub fn derive_json_schema(input: TokenStream) -> TokenStream {
- let input = parse_macro_input!(input as DeriveInput);
+ let input = syn::parse_macro_input!(input as DeriveInput);
let name = &input.ident;
- let fields = match &input.data {
+ let schema_body = match &input.data {
Data::Struct(data) => match &data.fields {
- Fields::Named(fields) => &fields.named,
- _ => panic!("JsonSchema derive macro only supports named fields"),
+ Fields::Named(fields) => {
+ let field_entries = fields.named.iter().map(|field| {
+ let field_attrs = &field.attrs;
+ let renamed_field = renamed_field(field_attrs);
+ let field_name =
+ renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
+ let field_type = &field.ty;
+
+ let schema = type_to_json_schema(field_type, field_attrs);
+ quote! {
+ properties.insert(
+ #field_name.to_string(),
+ serde_json::Value::Object(#schema)
+ );
+ }
+ });
+
+ let required_fields = fields.named.iter().filter_map(|field| {
+ let renamed_field = renamed_field(&field.attrs);
+ let field_name =
+ renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
+
+ let field_type = &field.ty;
+ if !is_option(field_type) {
+ Some(quote! {
+ required.push(#field_name.to_string());
+ })
+ } else {
+ None
+ }
+ });
+
+ quote! {
+ let mut schema = serde_json::Map::new();
+ let mut properties = serde_json::Map::new();
+ let mut required = Vec::new();
+
+ #(#field_entries)*
+
+ #(#required_fields)*
+
+ schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
+ schema.insert("properties".to_string(), serde_json::Value::Object(properties));
+ if !required.is_empty() {
+ schema.insert("required".to_string(), serde_json::Value::Array(
+ required.into_iter().map(serde_json::Value::String).collect()
+ ));
+ }
+
+ schema
+ }
+ }
+ _ => panic!("JsonSchema derive macro only supports named fields for structs"),
},
- _ => panic!("JsonSchema derive macro only supports structs"),
- };
+ Data::Enum(data) => {
+ let variant_schemas = data.variants.iter().map(|variant| {
+ let variant_attrs = &variant.attrs;
+ let variant_name = variant.ident.to_string();
+ let renamed_variant = renamed_field(variant_attrs).unwrap_or(variant_name.clone());
- let field_entries = fields.iter().map(|field| {
- let field_attrs = &field.attrs;
- let renamed_field = renamed_field(field_attrs);
- let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
- let field_type = &field.ty;
+ // Parse variant-level json_schema attributes
+ let mut title: Option = None;
+ let mut description: Option = None;
+ for attr in variant_attrs {
+ if attr.path().is_ident("json_schema") {
+ let _ = attr.parse_nested_meta(|meta| {
+ if meta.path.is_ident("title") {
+ title = Some(meta.value()?.parse::()?.value());
+ } else if meta.path.is_ident("description") {
+ description = Some(meta.value()?.parse::()?.value());
+ }
+ Ok(())
+ });
+ }
+ }
- let schema = type_to_json_schema(field_type, field_attrs);
- quote! {
- properties.insert(
- #field_name.to_string(),
- serde_json::Value::Object(#schema)
- );
- }
- });
+ let title_quote = title.as_ref().map(|t| {
+ quote! { map.insert("title".to_string(), serde_json::Value::String(#t.to_string())); }
+ });
+ let description_quote = description.as_ref().map(|desc| {
+ quote! { map.insert("description".to_string(), serde_json::Value::String(#desc.to_string())); }
+ });
- let required_fields = fields.iter().filter_map(|field| {
- let renamed_field = renamed_field(&field.attrs);
- let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
+ match &variant.fields {
+ Fields::Unit => {
+ // Unit variant: use "enum" with the variant name
+ quote! {
+ {
+ let mut map = serde_json::Map::new();
+ map.insert("enum".to_string(), serde_json::Value::Array(vec![
+ serde_json::Value::String(#renamed_variant.to_string())
+ ]));
+ #title_quote
+ #description_quote
+ serde_json::Value::Object(map)
+ }
+ }
+ }
+ Fields::Unnamed(fields) => {
+ // Newtype or tuple variant
+ if fields.unnamed.len() == 1 {
+ // Newtype variant: use the inner type's schema
+ let field = &fields.unnamed[0];
+ let field_type = &field.ty;
+ let field_attrs = &field.attrs;
+ let schema = type_to_json_schema(field_type, field_attrs);
+ quote! {
+ {
+ let mut map = #schema;
+ #title_quote
+ #description_quote
+ serde_json::Value::Object(map)
+ }
+ }
+ } else {
+ // Tuple variant: array with items
+ let field_schemas = fields.unnamed.iter().map(|field| {
+ let field_type = &field.ty;
+ let field_attrs = &field.attrs;
+ let schema = type_to_json_schema(field_type, field_attrs);
+ quote! { serde_json::Value::Object(#schema) }
+ });
+ quote! {
+ {
+ let mut map = serde_json::Map::new();
+ map.insert("type".to_string(), serde_json::Value::String("array".to_string()));
+ map.insert("items".to_string(), serde_json::Value::Array(vec![#(#field_schemas),*]));
+ map.insert("additionalItems".to_string(), serde_json::Value::Bool(false));
+ #title_quote
+ #description_quote
+ serde_json::Value::Object(map)
+ }
+ }
+ }
+ }
+ Fields::Named(fields) => {
+ // Struct variant: object with properties and required fields
+ let field_entries = fields.named.iter().map(|field| {
+ let field_attrs = &field.attrs;
+ let renamed_field = renamed_field(field_attrs);
+ let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
+ let field_type = &field.ty;
- let field_type = &field.ty;
- if !is_option(field_type) {
- Some(quote! {
- required.push(#field_name.to_string());
- })
- } else {
- None
- }
- });
+ let schema = type_to_json_schema(field_type, field_attrs);
+ quote! {
+ properties.insert(
+ #field_name.to_string(),
+ serde_json::Value::Object(#schema)
+ );
+ }
+ });
- let expanded = quote! {
- impl #name {
- pub fn json_schema() -> serde_json::Map {
- let mut schema = serde_json::Map::new();
- let mut properties = serde_json::Map::new();
- let mut required = Vec::new();
+ let required_fields = fields.named.iter().filter_map(|field| {
+ let renamed_field = renamed_field(&field.attrs);
+ let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
+
+ let field_type = &field.ty;
+ if !is_option(field_type) {
+ Some(quote! {
+ required.push(#field_name.to_string());
+ })
+ } else {
+ None
+ }
+ });
- #(#field_entries)*
+ quote! {
+ {
+ let mut map = serde_json::Map::new();
+ let mut properties = serde_json::Map::new();
+ let mut required = Vec::new();
- #(#required_fields)*
+ #(#field_entries)*
- schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
- schema.insert("properties".to_string(), serde_json::Value::Object(properties));
- if !required.is_empty() {
- schema.insert("required".to_string(), serde_json::Value::Array(
- required.into_iter().map(serde_json::Value::String).collect()
- ));
+ #(#required_fields)*
+
+ map.insert("type".to_string(), serde_json::Value::String("object".to_string()));
+ map.insert("properties".to_string(), serde_json::Value::Object(properties));
+ if !required.is_empty() {
+ map.insert("required".to_string(), serde_json::Value::Array(
+ required.into_iter().map(serde_json::Value::String).collect()
+ ));
+ }
+ #title_quote
+ #description_quote
+ serde_json::Value::Object(map)
+ }
+ }
+ }
}
+ });
+ quote! {
+ let mut schema = serde_json::Map::new();
+ schema.insert("oneOf".to_string(), serde_json::Value::Array(vec![
+ #(#variant_schemas),*
+ ]));
schema
}
}
+ _ => panic!("JsonSchema derive macro only supports structs and enums"),
+ };
+
+ let expanded = quote! {
+ impl #name {
+ pub fn json_schema() -> serde_json::Map {
+ #schema_body
+ }
+ }
};
TokenStream::from(expanded)
}
diff --git a/crates/rust-mcp-macros/src/utils.rs b/crates/rust-mcp-macros/src/utils.rs
index 0d4bbed..71d3de3 100644
--- a/crates/rust-mcp-macros/src/utils.rs
+++ b/crates/rust-mcp-macros/src/utils.rs
@@ -1,5 +1,8 @@
use quote::quote;
-use syn::{punctuated::Punctuated, token, Attribute, Path, PathArguments, Type};
+use syn::{
+ punctuated::Punctuated, token, Attribute, DeriveInput, Lit, LitInt, LitStr, Path,
+ PathArguments, Type,
+};
// Check if a type is an Option
pub fn is_option(ty: &Type) -> bool {
@@ -13,8 +16,8 @@ pub fn is_option(ty: &Type) -> bool {
false
}
-// Check if a type is a Vec
#[allow(unused)]
+// Check if a type is a Vec
pub fn is_vec(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 {
@@ -26,8 +29,8 @@ pub fn is_vec(ty: &Type) -> bool {
false
}
-// Extract the inner type from Vec or Option
#[allow(unused)]
+// Extract the inner type from Vec or Option
pub fn inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 {
@@ -46,12 +49,11 @@ pub fn inner_type(ty: &Type) -> Option<&Type> {
None
}
-fn doc_comment(attrs: &[Attribute]) -> Option {
+pub fn doc_comment(attrs: &[Attribute]) -> Option {
let mut docs = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &attr.meta {
- // Match value as Expr::Lit, then extract Lit::Str
if let syn::Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
docs.push(lit_str.value().trim().to_string());
@@ -82,16 +84,143 @@ pub fn might_be_struct(ty: &Type) -> bool {
false
}
+// Helper to check if a type is an enum
+pub fn is_enum(ty: &Type, _input: &DeriveInput) -> bool {
+ if let Type::Path(type_path) = ty {
+ // Check for #[mcp_elicit(enum)] attribute on the type
+ // Since we can't access the enum's definition directly, we rely on the attribute
+ // This assumes the enum is marked with #[mcp_elicit(enum)] in its definition
+ // Alternatively, we could pass a list of known enums, but attribute-based is simpler
+ type_path
+ .path
+ .segments
+ .last()
+ .map(|s| {
+ // For now, we'll assume any type could be an enum if it has the attribute
+ // In a real-world scenario, we'd need to resolve the type's definition
+ // For simplicity, we check if the type name is plausible (not String, bool, i32, i64)
+ let ident = s.ident.to_string();
+ !["String", "bool", "i32", "i64"].contains(&ident.as_str())
+ })
+ .unwrap_or(false)
+ } else {
+ false
+ }
+}
+
+// Helper to generate enum parsing code
+pub fn generate_enum_parse(
+ field_type: &Type,
+ field_name: &str,
+ base_crate: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ let type_ident = match field_type {
+ Type::Path(type_path) => type_path.path.segments.last().unwrap().ident.clone(),
+ _ => panic!("Expected path type for enum"),
+ };
+ // Since we can't access the enum's variants directly in this context,
+ // we'll assume the enum has unit variants and expect strings matching their names
+ // In a real-world scenario, you'd parse the enum's Data::Enum to get variant names
+ // For now, we'll generate a generic parse assuming variant names are provided as strings
+ quote! {
+ {
+ // Attempt to parse the string using a match
+ // Since we don't have the variants, we rely on the enum implementing FromStr
+ match s.as_str() {
+ // We can't dynamically list variants, so we use FromStr
+ // If FromStr is not implemented, this will fail at compile time
+ s => s.parse().map_err(|_| #base_crate::RpcError::parse_error().with_message(format!(
+ "Invalid enum value for field '{}': cannot parse '{}' into {}",
+ #field_name, s, stringify!(#type_ident)
+ )))?
+ }
+ }
+ }
+}
+
pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::TokenStream {
- let number_types = [
- "i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128", "f32", "f64",
+ let integer_types = [
+ "i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128",
];
- let doc_comment = doc_comment(attrs);
- let description = doc_comment.as_ref().map(|desc| {
+ let float_types = ["f32", "f64"];
+
+ // Parse custom json_schema attributes
+ let mut title: Option = None;
+ let mut format: Option = None;
+ let mut min_length: Option = None;
+ let mut max_length: Option = None;
+ let mut minimum: Option = None;
+ let mut maximum: Option = None;
+ let mut default: Option = None;
+ let mut attr_description: Option = None;
+
+ for attr in attrs {
+ if attr.path().is_ident("json_schema") {
+ let _ = attr.parse_nested_meta(|meta| {
+ if meta.path.is_ident("title") {
+ title = Some(meta.value()?.parse::()?.value());
+ } else if meta.path.is_ident("description") {
+ attr_description = Some(meta.value()?.parse::()?.value());
+ } else if meta.path.is_ident("format") {
+ format = Some(meta.value()?.parse::()?.value());
+ } else if meta.path.is_ident("min_length") {
+ min_length = Some(meta.value()?.parse::()?.base10_parse::()?);
+ } else if meta.path.is_ident("max_length") {
+ max_length = Some(meta.value()?.parse::()?.base10_parse::()?);
+ } else if meta.path.is_ident("minimum") {
+ minimum = Some(meta.value()?.parse::()?.base10_parse::()?);
+ } else if meta.path.is_ident("maximum") {
+ maximum = Some(meta.value()?.parse::()?.base10_parse::()?);
+ } else if meta.path.is_ident("default") {
+ let lit = meta.value()?.parse::()?;
+ default = Some(match lit {
+ Lit::Str(lit_str) => {
+ let value = lit_str.value();
+ quote! { serde_json::Value::String(#value.to_string()) }
+ }
+ Lit::Int(lit_int) => {
+ let value = lit_int.base10_parse::()?;
+ assert!(
+ (i64::MIN..=i64::MAX).contains(&value),
+ "Default value {value} out of range for i64"
+ );
+ quote! { serde_json::Value::Number(serde_json::Number::from(#value)) }
+ }
+ Lit::Float(lit_float) => {
+ let value = lit_float.base10_parse::()?;
+ quote! { serde_json::Value::Number(serde_json::Number::from_f64(#value).expect("Invalid float")) }
+ }
+ Lit::Bool(lit_bool) => {
+ let value = lit_bool.value();
+ quote! { serde_json::Value::Bool(#value) }
+ }
+ _ => return Err(meta.error("Unsupported default value type")),
+ });
+ }
+ Ok(())
+ });
+ }
+ }
+
+ let description = attr_description.or(doc_comment(attrs));
+ let description_quote = description.as_ref().map(|desc| {
quote! {
map.insert("description".to_string(), serde_json::Value::String(#desc.to_string()));
}
});
+
+ let title_quote = title.as_ref().map(|t| {
+ quote! {
+ map.insert("title".to_string(), serde_json::Value::String(#t.to_string()));
+ }
+ });
+
+ let default_quote = default.as_ref().map(|d| {
+ quote! {
+ map.insert("default".to_string(), #d);
+ }
+ });
+
match ty {
Type::Path(type_path) => {
if type_path.path.segments.len() == 1 {
@@ -104,15 +233,43 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
if args.args.len() == 1 {
if let syn::GenericArgument::Type(inner_ty) = &args.args[0] {
let inner_schema = type_to_json_schema(inner_ty, attrs);
+ let format_quote = format.as_ref().map(|f| {
+ quote! {
+ map.insert("format".to_string(), serde_json::Value::String(#f.to_string()));
+ }
+ });
+ let min_quote = min_length.as_ref().map(|min| {
+ quote! {
+ map.insert("minLength".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_quote = max_length.as_ref().map(|max| {
+ quote! {
+ map.insert("maxLength".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
+ let min_num_quote = minimum.as_ref().map(|min| {
+ quote! {
+ map.insert("minimum".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_num_quote = maximum.as_ref().map(|max| {
+ quote! {
+ map.insert("maximum".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
return quote! {
{
- let mut map = serde_json::Map::new();
- let inner_map = #inner_schema;
- for (k, v) in inner_map {
- map.insert(k, v);
- }
+ let mut map = #inner_schema;
map.insert("nullable".to_string(), serde_json::Value::Bool(true));
- #description
+ #description_quote
+ #title_quote
+ #format_quote
+ #min_quote
+ #max_quote
+ #min_num_quote
+ #max_num_quote
+ #default_quote
map
}
};
@@ -126,12 +283,26 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
if args.args.len() == 1 {
if let syn::GenericArgument::Type(inner_ty) = &args.args[0] {
let inner_schema = type_to_json_schema(inner_ty, &[]);
+ let min_quote = min_length.as_ref().map(|min| {
+ quote! {
+ map.insert("minItems".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_quote = max_length.as_ref().map(|max| {
+ quote! {
+ map.insert("maxItems".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
return quote! {
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("array".to_string()));
map.insert("items".to_string(), serde_json::Value::Object(#inner_schema));
- #description
+ #description_quote
+ #title_quote
+ #min_quote
+ #max_quote
+ #default_quote
map
}
};
@@ -144,36 +315,104 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
let path = &type_path.path;
return quote! {
{
- let inner_schema = #path::json_schema();
- inner_schema
+ let mut map = #path::json_schema();
+ #description_quote
+ #title_quote
+ #default_quote
+ map
}
};
}
- // Handle basic types
+ // Handle String
else if ident == "String" {
+ let format_quote = format.as_ref().map(|f| {
+ quote! {
+ map.insert("format".to_string(), serde_json::Value::String(#f.to_string()));
+ }
+ });
+ let min_quote = min_length.as_ref().map(|min| {
+ quote! {
+ map.insert("minLength".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_quote = max_length.as_ref().map(|max| {
+ quote! {
+ map.insert("maxLength".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
return quote! {
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("string".to_string()));
- #description
+ #description_quote
+ #title_quote
+ #format_quote
+ #min_quote
+ #max_quote
+ #default_quote
map
}
};
- } else if number_types.iter().any(|t| ident == t) {
+ }
+ // Handle integer types
+ else if integer_types.iter().any(|t| ident == t) {
+ let min_quote = minimum.as_ref().map(|min| {
+ quote! {
+ map.insert("minimum".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_quote = maximum.as_ref().map(|max| {
+ quote! {
+ map.insert("maximum".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
+ return quote! {
+ {
+ let mut map = serde_json::Map::new();
+ map.insert("type".to_string(), serde_json::Value::String("integer".to_string()));
+ #description_quote
+ #title_quote
+ #min_quote
+ #max_quote
+ #default_quote
+ map
+ }
+ };
+ }
+ // Handle float types
+ else if float_types.iter().any(|t| ident == t) {
+ let min_quote = minimum.as_ref().map(|min| {
+ quote! {
+ map.insert("minimum".to_string(), serde_json::Value::Number(serde_json::Number::from(#min)));
+ }
+ });
+ let max_quote = maximum.as_ref().map(|max| {
+ quote! {
+ map.insert("maximum".to_string(), serde_json::Value::Number(serde_json::Number::from(#max)));
+ }
+ });
return quote! {
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("number".to_string()));
- #description
+ #description_quote
+ #title_quote
+ #min_quote
+ #max_quote
+ #default_quote
map
}
};
- } else if ident == "bool" {
+ }
+ // Handle bool
+ else if ident == "bool" {
return quote! {
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("boolean".to_string()));
- #description
+ #description_quote
+ #title_quote
+ #default_quote
map
}
};
@@ -184,7 +423,9 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("unknown".to_string()));
- #description
+ #description_quote
+ #title_quote
+ #default_quote
map
}
}
@@ -193,7 +434,9 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("unknown".to_string()));
- #description
+ #description_quote
+ #title_quote
+ #default_quote
map
}
},
@@ -204,7 +447,6 @@ pub fn type_to_json_schema(ty: &Type, attrs: &[Attribute]) -> proc_macro2::Token
pub fn has_derive(attrs: &[Attribute], trait_name: &str) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("derive") {
- // Parse the derive arguments as a comma-separated list of paths
let parsed = attr.parse_args_with(Punctuated::::parse_terminated);
if let Ok(derive_paths) = parsed {
let derived = derive_paths.iter().any(|path| path.is_ident(trait_name));
@@ -220,7 +462,6 @@ pub fn renamed_field(attrs: &[Attribute]) -> Option {
for attr in attrs {
if attr.path().is_ident("serde") {
- // Ignore other serde meta items (e.g., skip_serializing_if)
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
if let Ok(lit) = meta.value() {
@@ -493,12 +734,12 @@ mod tests {
}
#[test]
- fn test_json_schema_number() {
+ fn test_json_schema_integer() {
let ty: syn::Type = parse_quote!(i32);
let tokens = type_to_json_schema(&ty, &[]);
let output = render(tokens);
assert!(output
- .contains("\"type\".to_string(),serde_json::Value::String(\"number\".to_string())"));
+ .contains("\"type\".to_string(),serde_json::Value::String(\"integer\".to_string())"));
}
#[test]
@@ -527,7 +768,7 @@ mod tests {
let output = render(tokens);
assert!(output.contains("\"nullable\".to_string(),serde_json::Value::Bool(true)"));
assert!(output
- .contains("\"type\".to_string(),serde_json::Value::String(\"number\".to_string())"));
+ .contains("\"type\".to_string(),serde_json::Value::String(\"integer\".to_string())"));
}
#[test]
diff --git a/crates/rust-mcp-macros/tests/common/common.rs b/crates/rust-mcp-macros/tests/common/common.rs
index 40c4e3c..d6bae2e 100644
--- a/crates/rust-mcp-macros/tests/common/common.rs
+++ b/crates/rust-mcp-macros/tests/common/common.rs
@@ -1,4 +1,7 @@
+use std::str::FromStr;
+
use rust_mcp_macros::JsonSchema;
+use rust_mcp_schema::RpcError;
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, JsonSchema)]
/// Represents a text replacement operation.
@@ -26,3 +29,50 @@ pub struct EditFileTool {
)]
pub dry_run: Option,
}
+
+#[derive(JsonSchema, Debug)]
+pub enum Colors {
+ #[json_schema(title = "Green Color")]
+ Green,
+ #[json_schema(title = "Red Color")]
+ Red,
+}
+
+impl FromStr for Colors {
+ type Err = RpcError;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "green" => Ok(Colors::Green),
+ "red" => Ok(Colors::Red),
+ _ => Err(RpcError::parse_error().with_message("Invalid color".to_string())),
+ }
+ }
+}
+
+#[mcp_elicit(message = "Please enter your info")]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(
+ title = "Name",
+ description = "The user's full name",
+ min_length = 5,
+ max_length = 100
+ )]
+ pub name: String,
+
+ /// Email address of the user
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+
+ /// The user's age in years
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+
+ /// Is user a student?
+ #[json_schema(title = "Is student?", default = true)]
+ pub is_student: Option,
+
+ /// User's favorite color
+ pub favorate_color: Colors,
+}
diff --git a/crates/rust-mcp-macros/tests/macro_test.rs b/crates/rust-mcp-macros/tests/macro_test.rs
index 3a23c87..4b6c926 100644
--- a/crates/rust-mcp-macros/tests/macro_test.rs
+++ b/crates/rust-mcp-macros/tests/macro_test.rs
@@ -1,4 +1,16 @@
+#[macro_use]
+extern crate rust_mcp_macros;
+
+use std::collections::HashMap;
+
use common::EditOperation;
+use rust_mcp_schema::{
+ BooleanSchema, ElicitRequestedSchema, ElicitResultContentValue, EnumSchema, NumberSchema,
+ PrimitiveSchemaDefinition, StringSchema, StringSchemaFormat,
+};
+use serde_json::json;
+
+use crate::common::{Colors, UserInfo};
#[path = "common/common.rs"]
pub mod common;
@@ -31,3 +43,232 @@ fn test_rename() {
let properties = schema.get("properties").unwrap().as_object().unwrap();
assert_eq!(properties.len(), 2);
}
+
+#[test]
+fn test_attributes() {
+ #[derive(JsonSchema)]
+ struct User {
+ /// This is a fallback description from doc comment.
+ pub id: i32,
+
+ #[json_schema(
+ title = "User Name",
+ description = "The user's full name (overrides doc)",
+ min_length = 1,
+ max_length = 100
+ )]
+ pub name: String,
+
+ #[json_schema(
+ title = "User Email",
+ format = "email",
+ min_length = 5,
+ max_length = 255
+ )]
+ pub email: Option,
+
+ #[json_schema(
+ title = "Tags",
+ description = "List of tags",
+ min_length = 0,
+ max_length = 10
+ )]
+ pub tags: Vec,
+ }
+
+ let schema = User::json_schema();
+ let expected = json!({
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "This is a fallback description from doc comment."
+ },
+ "name": {
+ "type": "string",
+ "title": "User Name",
+ "description": "The user's full name (overrides doc)",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "email": {
+ "type": "string",
+ "title": "User Email",
+ "format": "email",
+ "minLength": 5,
+ "maxLength": 255,
+ "nullable": true
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ },
+ "title": "Tags",
+ "description": "List of tags",
+ "minItems": 0,
+ "maxItems": 10
+ }
+ },
+ "required": ["id", "name", "tags"]
+ });
+
+ // Convert expected_value from serde_json::Value to serde_json::Map
+ let expected: serde_json::Map =
+ expected.as_object().expect("Expected JSON object").clone();
+
+ assert_eq!(schema, expected);
+}
+
+#[test]
+fn test_elicit_macro() {
+ assert_eq!(UserInfo::message(), "Please enter your info");
+
+ let requested_schema: ElicitRequestedSchema = UserInfo::requested_schema();
+ assert_eq!(
+ requested_schema.required,
+ vec!["name", "age", "favorate_color"]
+ );
+
+ assert!(matches!(
+ requested_schema.properties.get("is_student").unwrap(),
+ PrimitiveSchemaDefinition::BooleanSchema(BooleanSchema {
+ default,
+ description,
+ title,
+ ..
+ })
+ if
+ description.as_ref().unwrap() == "Is user a student?" &&
+ title.as_ref().unwrap() == "Is student?" &&
+ matches!(default, Some(true))
+
+ ));
+
+ assert!(matches!(
+ requested_schema.properties.get("favorate_color").unwrap(),
+ PrimitiveSchemaDefinition::EnumSchema(EnumSchema {
+ description,
+ enum_,
+ enum_names,
+ title,
+ ..
+ })
+ if description.as_ref().unwrap() == "User's favorite color" &&
+ title.is_none() &&
+ enum_.len()==2 && enum_.iter().all(|s| ["Green", "Red"].contains(&s.as_str())) &&
+ enum_names.len()==2 && enum_names.iter().all(|s| ["Green Color", "Red Color"].contains(&s.as_str()))
+ ));
+
+ assert!(matches!(
+ requested_schema.properties.get("age").unwrap(),
+ PrimitiveSchemaDefinition::NumberSchema(NumberSchema {
+ description,
+ maximum,
+ minimum,
+ title,
+ type_
+ })
+ if
+ description.as_ref().unwrap() == "The user's age in years" &&
+ maximum.unwrap() == 125 && minimum.unwrap() == 15 && title.as_ref().unwrap() == "Age"
+ ));
+
+ assert!(matches!(
+ requested_schema.properties.get("name").unwrap(),
+ PrimitiveSchemaDefinition::StringSchema(StringSchema {
+ description,
+ format,
+ max_length,
+ min_length,
+ title,
+ ..
+ })
+ if format.is_none() &&
+ description.as_ref().unwrap() == "The user's full name" &&
+ max_length.unwrap() == 100 && min_length.unwrap() == 5 && title.as_ref().unwrap() == "Name"
+ ));
+
+ assert!(matches!(
+ requested_schema.properties.get("email").unwrap(),
+ PrimitiveSchemaDefinition::StringSchema(StringSchema {
+ description,
+ format,
+ max_length,
+ min_length,
+ title,
+ ..
+ }) if matches!(format.unwrap(), StringSchemaFormat::Email) &&
+ description.as_ref().unwrap() == "Email address of the user" &&
+ max_length.is_none() && min_length.is_none() && title.as_ref().unwrap() == "Email"
+ ));
+
+ let json_schema = &UserInfo::json_schema();
+
+ let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
+ Some(arr) => arr
+ .iter()
+ .filter_map(|item| item.as_str().map(String::from))
+ .collect(),
+ None => Vec::new(),
+ };
+
+ let properties: Option> = json_schema
+ .get("properties")
+ .and_then(|v| v.as_object()) // Safely extract "properties" as an object.
+ .map(|properties| {
+ properties
+ .iter()
+ .filter_map(|(key, value)| {
+ serde_json::to_value(value)
+ .ok() // If serialization fails, return None.
+ .and_then(|v| {
+ if let serde_json::Value::Object(obj) = v {
+ Some(obj)
+ } else {
+ None
+ }
+ })
+ .map(|obj| (key.to_string(), PrimitiveSchemaDefinition::try_from(&obj)))
+ })
+ .collect()
+ });
+
+ let properties = properties
+ .map(|map| {
+ map.into_iter()
+ .map(|(k, v)| v.map(|ok_v| (k, ok_v))) // flip Result inside tuple
+ .collect::, _>>() // collect only if all Ok
+ })
+ .transpose()
+ .unwrap();
+
+ let properties = properties.expect("Was not able to create a ElicitRequestedSchema");
+
+ ElicitRequestedSchema::new(properties, required);
+}
+
+#[test]
+fn test_from_content_map() {
+ let mut content: ::std::collections::HashMap<::std::string::String, ElicitResultContentValue> =
+ HashMap::new();
+
+ content.extend([
+ (
+ "name".to_string(),
+ ElicitResultContentValue::String("Ali".to_string()),
+ ),
+ (
+ "favorate_color".to_string(),
+ ElicitResultContentValue::String("Green".to_string()),
+ ),
+ ("age".to_string(), ElicitResultContentValue::Integer(15)),
+ (
+ "is_student".to_string(),
+ ElicitResultContentValue::Boolean(false),
+ ),
+ ]);
+
+ let u: UserInfo = UserInfo::from_content_map(Some(content)).unwrap();
+ assert!(matches!(u.favorate_color, Colors::Green));
+}
diff --git a/crates/rust-mcp-sdk/CHANGELOG.md b/crates/rust-mcp-sdk/CHANGELOG.md
index 8f2f4f7..4fde908 100644
--- a/crates/rust-mcp-sdk/CHANGELOG.md
+++ b/crates/rust-mcp-sdk/CHANGELOG.md
@@ -1,5 +1,68 @@
# Changelog
+## [0.7.0](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.6.3...rust-mcp-sdk-v0.7.0) (2025-09-19)
+
+
+### β BREAKING CHANGES
+
+* add Streamable HTTP Client , multiple refactoring and improvements ([#98](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/98))
+* update ServerHandler and ServerHandlerCore traits ([#96](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/96))
+
+### π Features
+
+* Add elicitation macros and add elicit_input() method ([#99](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/99)) ([3ab5fe7](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/3ab5fe73aaa10de2b5b23caee357ac15b37c845f))
+* Add Streamable HTTP Client , multiple refactoring and improvements ([#98](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/98)) ([abb0c36](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/abb0c36126b0a397bc20a1de36c5a5a80924a01e))
+* Add tls-no-provider feature ([#97](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/97)) ([5dacceb](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/5dacceb0c2d18b8334744a13d438c6916bb7244c))
+* Event store support for resumability ([#101](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/101)) ([08742bb](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/08742bb9636f81ee79eda4edc192b3b8ed4c7287))
+* Update ServerHandler and ServerHandlerCore traits ([#96](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/96)) ([a2d6d23](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/a2d6d23ab59fbc34d04526e2606f747f93a8468c))
+
+## [0.6.3](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.6.2...rust-mcp-sdk-v0.6.3) (2025-08-31)
+
+## [0.6.2](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.6.1...rust-mcp-sdk-v0.6.2) (2025-08-30)
+
+
+### π Bug Fixes
+
+* Tool-box macro panic on invalid requests ([#92](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/92)) ([54cc8ed](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/54cc8edb55c41455dd9211f296560e7a792a7b9c))
+
+## [0.6.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.6.0...rust-mcp-sdk-v0.6.1) (2025-08-28)
+
+
+### π Bug Fixes
+
+* Session ID access in handlers and add helper for listing active ([#90](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/90)) ([f2f0afb](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/f2f0afb542f6ff036a28cf01e102b27ce940665b))
+
+## [0.6.0](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.5.3...rust-mcp-sdk-v0.6.0) (2025-08-19)
+
+
+### β BREAKING CHANGES
+
+* improve request ID generation, remove deprecated methods and adding improvements
+
+### π Features
+
+* Improve request ID generation, remove deprecated methods and adding improvements ([95b91aa](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/95b91aad191e1b8777ca4a02612ab9183e0276d3))
+
+## [0.5.3](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.5.2...rust-mcp-sdk-v0.5.3) (2025-08-19)
+
+
+### π Bug Fixes
+
+* Handle missing client details and abort keep-alive task on drop ([#83](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/83)) ([308b1db](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/308b1dbd1744ff06046902303d8bcd6c3a92ffbe))
+
+## [0.5.2](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.5.1...rust-mcp-sdk-v0.5.2) (2025-08-16)
+
+
+### π Features
+
+* Integrate list root and client info into hyper runtime ([36dfa4c](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/36dfa4cdc821e958ffe78b909ed28f5577d113c8))
+
+
+### π Bug Fixes
+
+* Abort keep-alive task when transport is removed ([#82](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/82)) ([1ca8e49](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/1ca8e49860e990c3562623e75dd723b0d1dc8256))
+* Ensure server-initiated requests include a valid request_id ([#80](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/80)) ([5f9a966](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/5f9a966bb523bf61daefcff209199bc774fa5ed6))
+
## [0.5.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.5.0...rust-mcp-sdk-v0.5.1) (2025-08-12)
diff --git a/crates/rust-mcp-sdk/Cargo.toml b/crates/rust-mcp-sdk/Cargo.toml
index 5f28fa3..8bba7c7 100644
--- a/crates/rust-mcp-sdk/Cargo.toml
+++ b/crates/rust-mcp-sdk/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rust-mcp-sdk"
-version = "0.5.1"
+version = "0.7.0"
authors = ["Ali Hashemi"]
categories = ["data-structures", "parser-implementations", "parsing"]
description = "An asynchronous SDK and framework for building MCP-Servers and MCP-Clients, leveraging the rust-mcp-schema for type safe MCP Schema Objects."
@@ -24,15 +24,17 @@ futures = { workspace = true }
thiserror = { workspace = true }
axum = { workspace = true, optional = true }
-uuid = { workspace = true, features = ["v4"], optional = true }
+uuid = { workspace = true, features = ["v4"] }
tokio-stream = { workspace = true, optional = true }
axum-server = { version = "0.7", features = [], optional = true }
tracing.workspace = true
+base64.workspace = true
# rustls = { workspace = true, optional = true }
hyper = { version = "1.6.0", optional = true }
[dev-dependencies]
+wiremock = "0.5"
reqwest = { workspace = true, default-features = false, features = [
"stream",
"rustls-tls",
@@ -51,47 +53,55 @@ default = [
"client",
"server",
"macros",
+ "stdio",
+ "sse",
+ "streamable-http",
"hyper-server",
"ssl",
"2025_06_18",
] # All features enabled by default
-server = ["rust-mcp-transport/stdio"] # Server feature
-client = ["rust-mcp-transport/stdio", "rust-mcp-transport/sse"] # Client feature
-hyper-server = [
- "axum",
- "axum-server",
- "hyper",
- "server",
- "uuid",
- "tokio-stream",
- "rust-mcp-transport/sse",
-]
+
+sse = ["rust-mcp-transport/sse"]
+streamable-http = ["rust-mcp-transport/streamable-http"]
+stdio = ["rust-mcp-transport/stdio"]
+
+server = [] # Server feature
+client = [] # Client feature
+hyper-server = ["axum", "axum-server", "hyper", "server", "tokio-stream"]
ssl = ["axum-server/tls-rustls"]
+tls-no-provider = ["axum-server/tls-rustls-no-provider"]
macros = ["rust-mcp-macros/sdk"]
-# enables mcp protocol version 2025_06_18
-2025_06_18 = [
+# enables mcp protocol version 2025-06-18
+2025-06-18 = [
"rust-mcp-schema/2025_06_18",
"rust-mcp-macros/2025_06_18",
"rust-mcp-transport/2025_06_18",
"rust-mcp-schema/schema_utils",
]
+# Alias: allow users to use underscores instead of hyphens
+2025_06_18 = ["2025-06-18"]
# enables mcp protocol version 2025_03_26
-2025_03_26 = [
+2025-03-26 = [
"rust-mcp-schema/2025_03_26",
"rust-mcp-macros/2025_03_26",
"rust-mcp-transport/2025_03_26",
"rust-mcp-schema/schema_utils",
]
+# Alias: allow users to use underscores instead of hyphens
+2025_03_26 = ["2025-03-26"]
+
# enables mcp protocol version 2024_11_05
-2024_11_05 = [
+2024-11-05 = [
"rust-mcp-schema/2024_11_05",
"rust-mcp-macros/2024_11_05",
"rust-mcp-transport/2024_11_05",
"rust-mcp-schema/schema_utils",
]
+# Alias: allow users to use underscores instead of hyphens
+2024_11_05 = ["2024-11-05"]
[lints]
workspace = true
diff --git a/crates/rust-mcp-sdk/README.md b/crates/rust-mcp-sdk/README.md
index ef5b4ed..2c70c3e 100644
--- a/crates/rust-mcp-sdk/README.md
+++ b/crates/rust-mcp-sdk/README.md
@@ -9,7 +9,7 @@
[
](https://github.com/rust-mcp-stack/rust-mcp-sdk/actions/workflows/ci.yml)
[
-](examples/hello-world-mcp-server)
+](examples/hello-world-mcp-server-stdio)
A high-performance, asynchronous toolkit for building MCP servers and clients.
Focus on your app's logic while **rust-mcp-sdk** takes care of the rest!
@@ -32,15 +32,14 @@ This project supports following transports:
π The **rust-mcp-sdk** includes a lightweight [Axum](https://github.com/tokio-rs/axum) based server that handles all core functionality seamlessly. Switching between `stdio` and `Streamable HTTP` is straightforward, requiring minimal code changes. The server is designed to efficiently handle multiple concurrent client connections and offers built-in support for SSL.
-
**MCP Streamable HTTP Support**
- β
Streamable HTTP Support for MCP Servers
- β
DNS Rebinding Protection
- β
Batch Messages
- β
Streaming & non-streaming JSON response
-- β¬ Streamable HTTP Support for MCP Clients
-- β¬ Resumability
-- β¬ Authentication / Oauth
+- β
Streamable HTTP Support for MCP Clients
+- β
Resumability
+- β¬ Oauth Authentication
**β οΈ** Project is currently under development and should be used at your own risk.
@@ -49,7 +48,9 @@ This project supports following transports:
- [MCP Server (stdio)](#mcp-server-stdio)
- [MCP Server (Streamable HTTP)](#mcp-server-streamable-http)
- [MCP Client (stdio)](#mcp-client-stdio)
+ - [MCP Client (Streamable HTTP)](#mcp-client_streamable-http))
- [MCP Client (sse)](#mcp-client-sse)
+- [Macros](#macros)
- [Getting Started](#getting-started)
- [HyperServerOptions](#hyperserveroptions)
- [Security Considerations](#security-considerations)
@@ -110,7 +111,7 @@ async fn main() -> SdkResult<()> {
}
```
-See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
+See hello-world-mcp-server-stdio example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :

@@ -153,6 +154,7 @@ let server = hyper_server::create_server(
HyperServerOptions {
host: "127.0.0.1".to_string(),
sse_support: false,
+ event_store: Some(Arc::new(InMemoryEventStore::default())), // enable resumability
..Default::default()
},
);
@@ -180,7 +182,7 @@ pub struct MyServerHandler;
#[async_trait]
impl ServerHandler for MyServerHandler {
// Handle ListToolsRequest, return list of available tools as ListToolsResult
- async fn handle_list_tools_request(&self, request: ListToolsRequest, runtime: &dyn McpServer) -> Result {
+ async fn handle_list_tools_request(&self, request: ListToolsRequest, runtime: Arc) -> Result {
Ok(ListToolsResult {
tools: vec![SayHelloTool::tool()],
@@ -191,7 +193,7 @@ impl ServerHandler for MyServerHandler {
}
/// Handles requests to call a specific tool.
- async fn handle_call_tool_request( &self, request: CallToolRequest, runtime: &dyn McpServer, ) -> Result {
+ async fn handle_call_tool_request( &self, request: CallToolRequest, runtime: Arc ) -> Result {
if request.tool_name() == SayHelloTool::tool_name() {
Ok( CallToolResult::text_content( vec![TextContent::from("Hello World!".to_string())] ))
@@ -205,7 +207,7 @@ impl ServerHandler for MyServerHandler {
---
-π For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)**
+π For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)**
See hello-world-server-streamable-http example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
@@ -283,6 +285,8 @@ async fn main() -> SdkResult<()> {
println!("{}",result.content.first().unwrap().as_text_content()?.text);
+ client.shut_down().await?;
+
Ok(())
}
@@ -294,8 +298,82 @@ Here is the output :
> your results may vary slightly depending on the version of the MCP Server in use when you run it.
+### MCP Client (Streamable HTTP)
+```rs
+
+// STEP 1: Custom Handler to handle incoming MCP Messages
+pub struct MyClientHandler;
+
+#[async_trait]
+impl ClientHandler for MyClientHandler {
+ // To check out a list of all the methods in the trait that you can override, take a look at https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_client_handler.rs
+}
+
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+
+ // Step2 : Define client details and capabilities
+ let client_details: InitializeRequestParams = InitializeRequestParams {
+ capabilities: ClientCapabilities::default(),
+ client_info: Implementation {
+ name: "simple-rust-mcp-client-sse".to_string(),
+ version: "0.1.0".to_string(),
+ title: Some("Simple Rust MCP Client (SSE)".to_string()),
+ },
+ protocol_version: LATEST_PROTOCOL_VERSION.into(),
+ };
+
+ // Step 3: Create transport options to connect to an MCP server via Streamable HTTP.
+ let transport_options = StreamableTransportOptions {
+ mcp_url: MCP_SERVER_URL.to_string(),
+ request_options: RequestOptions {
+ ..RequestOptions::default()
+ },
+ };
+
+ // STEP 4: instantiate the custom handler that is responsible for handling MCP messages
+ let handler = MyClientHandler {};
+
+ // STEP 5: create the client with transport options and the handler
+ let client = client_runtime::with_transport_options(client_details, transport_options, handler);
+
+ // STEP 6: start the MCP client
+ client.clone().start().await?;
+
+ // STEP 7: use client methods to communicate with the MCP Server as you wish
+
+ // Retrieve and display the list of tools available on the server
+ let server_version = client.server_version().unwrap();
+ let tools = client.list_tools(None).await?.tools;
+ println!("List of tools for {}@{}", server_version.name, server_version.version);
+
+ tools.iter().enumerate().for_each(|(tool_index, tool)| {
+ println!(" {}. {} : {}",
+ tool_index + 1,
+ tool.name,
+ tool.description.clone().unwrap_or_default()
+ );
+ });
+
+ println!("Call \"add\" tool with 100 and 28 ...");
+ // Create a `Map` to represent the tool parameters
+ let params = json!({"a": 100,"b": 28}).as_object().unwrap().clone();
+ let request = CallToolRequestParams { name: "add".to_string(),arguments: Some(params)};
+
+ // invoke the tool
+ let result = client.call_tool(request).await?;
+
+ println!("{}",result.content.first().unwrap().as_text_content()?.text);
+
+ client.shut_down().await?;
+
+ Ok(())
+```
+π see [examples/simple-mcp-client-streamable-http](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-streamable-http) for a complete working example.
+
+
### MCP Client (sse)
-Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical, with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same:
+Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical to the [stdio example](#mcp-client-stdio) , with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same:
```diff
- let transport = StdioTransport::create_with_server_launch(
@@ -306,6 +384,116 @@ Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost
+ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?;
```
+π see [examples/simple-mcp-client-sse](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-sse) for a complete working example.
+
+
+## Macros
+[rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) includes several helpful macros that simplify common tasks when building MCP servers and clients. For example, they can automatically generate tool specifications and tool schemas right from your structs, or assist with elicitation requests and responses making them completely type safe.
+
+> To use these macros, ensure the `macros` feature is enabled in your Cargo.toml.
+
+### mcp_tool
+`mcp_tool` is a procedural macro attribute that helps generating rust_mcp_schema::Tool from a struct.
+
+Usage example:
+```rust
+#[mcp_tool(
+ name = "move_file",
+ title="Move File",
+ description = concat!("Move or rename files and directories. Can move files between directories ",
+"and rename them in a single operation. If the destination exists, the ",
+"operation will fail. Works across different directories and can be used ",
+"for simple renaming within the same directory. ",
+"Both source and destination must be within allowed directories."),
+ destructive_hint = false,
+ idempotent_hint = false,
+ open_world_hint = false,
+ read_only_hint = false
+)]
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, JsonSchema)]
+pub struct MoveFileTool {
+ /// The source path of the file to move.
+ pub source: String,
+ /// The destination path to move the file to.
+ pub destination: String,
+}
+
+// Now we can call `tool()` method on it to get a Tool instance
+let rust_mcp_sdk::schema::Tool = MoveFileTool::tool();
+
+```
+
+π» For a real-world example, check out any of the tools available at: https://github.com/rust-mcp-stack/rust-mcp-filesystem/tree/main/src/tools
+
+
+### tool_box
+`tool_box` generates an enum from a provided list of tools, making it easier to organize and manage them, especially when your application includes a large number of tools.
+
+It accepts an array of tools and generates an enum where each tool becomes a variant of the enum.
+
+Generated enum has a `tools()` function that returns a `Vec` , and a `TryFrom` trait implementation that could be used to convert a ToolRequest into a Tool instance.
+
+Usage example:
+```rust
+ // Accepts an array of tools and generates an enum named `FileSystemTools`,
+ // where each tool becomes a variant of the enum.
+ tool_box!(FileSystemTools, [ReadFileTool, MoveFileTool, SearchFilesTool]);
+
+ // now in the app, we can use the FileSystemTools, like:
+ let all_tools: Vec = FileSystemTools::tools();
+```
+
+π» To see a real-world example of that please see :
+- `tool_box` macro usage: [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/tools.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/tools.rs)
+- using `tools()` in list tools request : [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L67)
+- using `try_from` in call tool_request: [https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L100)
+
+
+
+### mcp_elicit
+The `mcp_elicit` macro generates implementations for the annotated struct to facilitate data elicitation. It enables struct to generate `ElicitRequestedSchema` and also parsing a map of field names to `ElicitResultContentValue` values back into the struct, supporting both required and optional fields. The generated implementation includes:
+
+- A `message()` method returning the elicitation message as a string.
+- A `requested_schema()` method returning an `ElicitRequestedSchema` based on the structβs JSON schema.
+- A `from_content_map()` method to convert a map of `ElicitResultContentValue` values into a struct instance.
+
+### Attributes
+
+- `message` - An optional string (or `concat!(...)` expression) to prompt the user or system for input. Defaults to an empty string if not provided.
+
+Usage example:
+```rust
+// A struct that could be used to send elicit request and get the input from the user
+#[mcp_elicit(message = "Please enter your info")]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(
+ title = "Name",
+ description = "The user's full name",
+ min_length = 5,
+ max_length = 100
+ )]
+ pub name: String,
+ /// Is user a student?
+ #[json_schema(title = "Is student?", default = true)]
+ pub is_student: Option,
+
+ /// User's favorite color
+ pub favorate_color: Colors,
+}
+
+// send a Elicit Request , ask for UserInfo data and convert the result back to a valid UserInfo instance
+let result: ElicitResult = server
+ .elicit_input(UserInfo::message(), UserInfo::requested_schema())
+ .await?;
+
+// Create a UserInfo instance using data provided by the user on the client side
+let user_info = UserInfo::from_content_map(result.content)?;
+
+```
+
+π» For mre info please see :
+- https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/crates/rust-mcp-macros
## Getting Started
@@ -337,6 +525,7 @@ server.start().await?;
Here is a list of available options with descriptions for configuring the HyperServer:
```rs
+
pub struct HyperServerOptions {
/// Hostname or IP address the server will bind to (default: "127.0.0.1")
pub host: String,
@@ -344,9 +533,19 @@ pub struct HyperServerOptions {
/// Hostname or IP address the server will bind to (default: "8080")
pub port: u16,
+ /// Optional thread-safe session id generator to generate unique session IDs.
+ pub session_id_generator: Option>>,
+
/// Optional custom path for the Streamable HTTP endpoint (default: `/mcp`)
pub custom_streamable_http_endpoint: Option,
+ /// Shared transport configuration used by the server
+ pub transport_options: Arc,
+
+ /// Event store for resumability support
+ /// If provided, resumability will be enabled, allowing clients to reconnect and resume messages
+ pub event_store: Option>,
+
/// This setting only applies to streamable HTTP.
/// If true, the server will return JSON responses instead of starting an SSE stream.
/// This can be useful for simple request/response scenarios without streaming.
@@ -356,12 +555,6 @@ pub struct HyperServerOptions {
/// Interval between automatic ping messages sent to clients to detect disconnects
pub ping_interval: Duration,
- /// Shared transport configuration used by the server
- pub transport_options: Arc,
-
- /// Optional thread-safe session id generator to generate unique session IDs.
- pub session_id_generator: Option>,
-
/// Enables SSL/TLS if set to `true`
pub enable_ssl: bool,
@@ -373,17 +566,6 @@ pub struct HyperServerOptions {
/// Required if `enable_ssl` is `true`.
pub ssl_key_path: Option,
- /// If set to true, the SSE transport will also be supported for backward compatibility (default: true)
- pub sse_support: bool,
-
- /// Optional custom path for the Server-Sent Events (SSE) endpoint (default: `/sse`)
- /// Applicable only if sse_support is true
- pub custom_sse_endpoint: Option,
-
- /// Optional custom path for the MCP messages endpoint for sse (default: `/messages`)
- /// Applicable only if sse_support is true
- pub custom_messages_endpoint: Option,
-
/// List of allowed host header values for DNS rebinding protection.
/// If not specified, host validation is disabled.
pub allowed_hosts: Option>,
@@ -395,6 +577,17 @@ pub struct HyperServerOptions {
/// Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured).
/// Default is false for backwards compatibility.
pub dns_rebinding_protection: bool,
+
+ /// If set to true, the SSE transport will also be supported for backward compatibility (default: true)
+ pub sse_support: bool,
+
+ /// Optional custom path for the Server-Sent Events (SSE) endpoint (default: `/sse`)
+ /// Applicable only if sse_support is true
+ pub custom_sse_endpoint: Option,
+
+ /// Optional custom path for the MCP messages endpoint for sse (default: `/messages`)
+ /// Applicable only if sse_support is true
+ pub custom_messages_endpoint: Option,
}
```
@@ -416,9 +609,15 @@ The `rust-mcp-sdk` crate provides several features that can be enabled or disabl
- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP servers.
- `client`: Activates MCP client capabilities, offering modules and APIs for client development and communicating with MCP servers.
-- `hyper-server`: This feature enables the **sse** transport for MCP servers, supporting multiple simultaneous client connections out of the box.
-- `ssl`: This feature enables TLS/SSL support for the **sse** transport when used with the `hyper-server`.
+- `hyper-server`: This feature is necessary to enable `Streamable HTTP` or `Server-Sent Events (SSE)` transports for MCP servers. It must be used alongside the server feature to support the required server functionalities.
+- `ssl`: This feature enables TLS/SSL support for the `Streamable HTTP` or `Server-Sent Events (SSE)` transport when used with the `hyper-server`.
- `macros`: Provides procedural macros for simplifying the creation and manipulation of MCP Tool structures.
+- `sse`: Enables support for the `Server-Sent Events (SSE)` transport.
+- `streamable-http`: Enables support for the `Streamable HTTP` transport.
+
+- `stdio`: Enables support for the `standard input/output (stdio)` transport.
+- `tls-no-provider`: Enables TLS without a crypto provider. This is useful if you are already using a different crypto provider than the aws-lc default.
+
#### MCP Protocol Versions with Corresponding Features
@@ -449,9 +648,9 @@ If you only need the MCP Server functionality, you can disable the default featu
```toml
[dependencies]
-rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros"] }
+rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros","stdio"] }
```
-Optionally add `hyper-server` for **sse** transport, and `ssl` feature for tls/ssl support of the `hyper-server`
+Optionally add `hyper-server` and `streamable-http` for **Streamable HTTP** transport, and `ssl` feature for tls/ssl support of the `hyper-server`
@@ -464,7 +663,7 @@ Add the following to your Cargo.toml:
```toml
[dependencies]
-rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["client","2024_11_05"] }
+rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["client","2024_11_05","stdio"] }
```
@@ -477,10 +676,10 @@ Learn when to use the `mcp_*_handler` traits versus the lower-level `mcp_*_hand
[rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) provides two type of handler traits that you can chose from:
- **ServerHandler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs.
- Refer to [examples/hello-world-mcp-server/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server/src/handler.rs) for an example.
+ Refer to [examples/hello-world-mcp-server-stdio/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio/src/handler.rs) for an example.
- **ServerHandlerCore**: If you need more control over MCP messages, consider using `ServerHandlerCore`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters.
- Refer to [examples/hello-world-mcp-server-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-core/src/handler.rs) for an example.
+ Refer to [examples/hello-world-mcp-server-stdio-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio-core/src/handler.rs) for an example.
---
@@ -509,7 +708,7 @@ Both functions create an MCP client instance.
-Check out the corresponding examples at: [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) and [examples/simple-mcp-client-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-core).
+Check out the corresponding examples at: [examples/simple-mcp-client-stdio](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-stdio) and [examples/simple-mcp-client-stdio-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-stdio-core).
## Projects using Rust MCP SDK
@@ -526,6 +725,11 @@ Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their na
|
| [text-to-cypher](https://github.com/FalkorDB/text-to-cypher) | A high-performance Rust-based API service that translates natural language text to Cypher queries for graph databases. | [GitHub](https://github.com/FalkorDB/text-to-cypher) |
|
| [notify-mcp](https://github.com/Tuurlijk/notify-mcp) | A Model Context Protocol (MCP) server that provides desktop notification functionality. | [GitHub](https://github.com/Tuurlijk/notify-mcp) |
|
| [lst](https://github.com/WismutHansen/lst) | `lst` is a personal lists, notes, and blog posts management application with a focus on plain-text storage, offline-first functionality, and multi-device synchronization. | [GitHub](https://github.com/WismutHansen/lst) |
+|
| [rust-mcp-server](https://github.com/Vaiz/rust-mcp-server) | `rust-mcp-server` allows the model to perform actions on your behalf, such as building, testing, and analyzing your Rust code. | [GitHub](https://github.com/Vaiz/rust-mcp-server) |
+
+
+
+
diff --git a/crates/rust-mcp-sdk/src/error.rs b/crates/rust-mcp-sdk/src/error.rs
index 2feab67..3879526 100644
--- a/crates/rust-mcp-sdk/src/error.rs
+++ b/crates/rust-mcp-sdk/src/error.rs
@@ -11,25 +11,36 @@ pub type SdkResult = core::result::Result;
#[derive(Debug, Error)]
pub enum McpSdkError {
+ #[error("Transport error: {0}")]
+ Transport(#[from] TransportError),
+
+ #[error("I/O error: {0}")]
+ Io(#[from] std::io::Error),
+
#[error("{0}")]
RpcError(#[from] RpcError),
+
#[error("{0}")]
- IoError(#[from] std::io::Error),
- #[error("{0}")]
- TransportError(#[from] TransportError),
- #[error("{0}")]
- JoinError(#[from] JoinError),
- #[error("{0}")]
- AnyError(Box<(dyn std::error::Error + Send + Sync)>),
- #[error("{0}")]
- SdkError(#[from] crate::schema::schema_utils::SdkError),
+ Join(#[from] JoinError),
+
#[cfg(feature = "hyper-server")]
#[error("{0}")]
- TransportServerError(#[from] TransportServerError),
- #[error("Incompatible mcp protocol version: requested:{0} current:{1}")]
- IncompatibleProtocolVersion(String, String),
+ HyperServer(#[from] TransportServerError),
+
#[error("{0}")]
- ParseProtocolVersionError(#[from] ParseProtocolVersionError),
+ SdkError(#[from] crate::schema::schema_utils::SdkError),
+
+ #[error("Protocol error: {kind}")]
+ Protocol { kind: ProtocolErrorKind },
+}
+
+// Sub-enum for protocol-related errors
+#[derive(Debug, Error)]
+pub enum ProtocolErrorKind {
+ #[error("Incompatible protocol version: requested {requested}, current {current}")]
+ IncompatibleVersion { requested: String, current: String },
+ #[error("Failed to parse protocol version: {0}")]
+ ParseError(#[from] ParseProtocolVersionError),
}
impl McpSdkError {
@@ -41,6 +52,3 @@ impl McpSdkError {
None
}
}
-
-#[deprecated(since = "0.2.0", note = "Use `McpSdkError` instead.")]
-pub type MCPSdkError = McpSdkError;
diff --git a/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs b/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs
index 0c1dcf3..f96b261 100644
--- a/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs
+++ b/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs
@@ -1,11 +1,12 @@
use std::{sync::Arc, time::Duration};
-use crate::schema::InitializeResult;
-use rust_mcp_transport::TransportOptions;
-
+use super::session_store::SessionStore;
use crate::mcp_traits::mcp_handler::McpServerHandler;
+use crate::{id_generator::FastIdGenerator, mcp_traits::IdGenerator, schema::InitializeResult};
+
+use rust_mcp_transport::event_store::EventStore;
-use super::{session_store::SessionStore, IdGenerator};
+use rust_mcp_transport::{SessionId, TransportOptions};
/// Application state struct for the Hyper server
///
@@ -14,7 +15,8 @@ use super::{session_store::SessionStore, IdGenerator};
#[derive(Clone)]
pub struct AppState {
pub session_store: Arc,
- pub id_generator: Arc,
+ pub id_generator: Arc>,
+ pub stream_id_gen: Arc,
pub server_details: Arc,
pub handler: Arc,
pub ping_interval: Duration,
@@ -31,6 +33,9 @@ pub struct AppState {
/// Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured).
/// Default is false for backwards compatibility.
pub dns_rebinding_protection: bool,
+ /// Event store for resumability support
+ /// If provided, resumability will be enabled, allowing clients to reconnect and resume messages
+ pub event_store: Option>,
}
impl AppState {
diff --git a/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs b/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs
index 30df951..85cf791 100644
--- a/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs
+++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs
@@ -4,7 +4,8 @@ use crate::{
mcp_server::HyperServer,
schema::{
schema_utils::{NotificationFromServer, RequestFromServer, ResultFromClient},
- CreateMessageRequestParams, CreateMessageResult, LoggingMessageNotificationParams,
+ CreateMessageRequestParams, CreateMessageResult, InitializeRequestParams,
+ ListRootsRequestParams, ListRootsResult, LoggingMessageNotificationParams,
PromptListChangedNotificationParams, ResourceListChangedNotificationParams,
ResourceUpdatedNotificationParams, ToolListChangedNotificationParams,
},
@@ -69,6 +70,12 @@ impl HyperRuntime {
result.map_err(|err| err.into())
}
+ /// Returns a list of active session IDs from the session store.
+ pub async fn sessions(&self) -> Vec {
+ self.state.session_store.keys().await
+ }
+
+ /// Retrieves the runtime associated with the given session ID from the session store.
pub async fn runtime_by_session(
&self,
session_id: &SessionId,
@@ -99,6 +106,21 @@ impl HyperRuntime {
runtime.send_notification(notification).await
}
+ /// Request a list of root URIs from the client. Roots allow
+ /// servers to ask for specific directories or files to operate on. A common example
+ /// for roots is providing a set of repositories or directories a server should operate on.
+ /// This request is typically used when the server needs to understand the file system
+ /// structure or access specific locations that the client has permission to read from
+ pub async fn list_roots(
+ &self,
+ session_id: &SessionId,
+ params: Option,
+ ) -> SdkResult {
+ let runtime = self.runtime_by_session(session_id).await?;
+ let runtime = runtime.lock().await.to_owned();
+ runtime.list_roots(params).await
+ }
+
pub async fn send_logging_message(
&self,
session_id: &SessionId,
@@ -195,4 +217,13 @@ impl HyperRuntime {
let runtime = runtime.lock().await.to_owned();
runtime.create_message(params).await
}
+
+ pub async fn client_info(
+ &self,
+ session_id: &SessionId,
+ ) -> SdkResult