diff --git a/.release-config.json b/.release-config.json
index 304ccc9..d87cd6a 100644
--- a/.release-config.json
+++ b/.release-config.json
@@ -65,6 +65,7 @@
"pull-request-header": ":robot: Auto-generated release PR",
"packages": {
"crates/rust-mcp-macros": {
+ "release-as": "0.8.0",
"release-type": "rust",
"draft": false,
"prerelease": false,
@@ -79,6 +80,7 @@
]
},
"crates/rust-mcp-transport": {
+ "release-as": "0.8.0",
"release-type": "rust",
"draft": false,
"prerelease": false,
@@ -92,7 +94,8 @@
}
]
},
- "crates/rust-mcp-extra": {
+ "crates/rust-mcp-sdk": {
+ "release-as": "0.8.0",
"release-type": "rust",
"draft": false,
"prerelease": false,
@@ -106,7 +109,7 @@
}
]
},
- "crates/rust-mcp-sdk": {
+ "crates/rust-mcp-extra": {
"release-type": "rust",
"draft": false,
"prerelease": false,
diff --git a/Cargo.lock b/Cargo.lock
index 314c813..8ce5772 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -120,7 +120,7 @@ dependencies = [
"bytes",
"form_urlencoded",
"futures-util",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -151,7 +151,7 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
dependencies = [
"bytes",
"futures-core",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"mime",
@@ -171,7 +171,7 @@ dependencies = [
"arc-swap",
"bytes",
"fs-err",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"hyper 1.8.1",
"hyper-util",
@@ -204,9 +204,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
-version = "1.8.0"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a"
[[package]]
name = "bitflags"
@@ -237,9 +237,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
-version = "1.2.47"
+version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -275,9 +275,9 @@ dependencies = [
[[package]]
name = "cmake"
-version = "0.1.54"
+version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586"
dependencies = [
"cc",
]
@@ -759,7 +759,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
- "http 1.3.1",
+ "http 1.4.0",
"indexmap",
"slab",
"tokio",
@@ -846,12 +846,11 @@ dependencies = [
[[package]]
name = "http"
-version = "1.3.1"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
- "fnv",
"itoa",
]
@@ -873,7 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
- "http 1.3.1",
+ "http 1.4.0",
]
[[package]]
@@ -884,7 +883,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -957,7 +956,7 @@ dependencies = [
"futures-channel",
"futures-core",
"h2 0.4.12",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"httparse",
"httpdate",
@@ -975,7 +974,7 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
- "http 1.3.1",
+ "http 1.4.0",
"hyper 1.8.1",
"hyper-util",
"rustls",
@@ -1004,16 +1003,16 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.18"
+version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
+checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"hyper 1.8.1",
"ipnet",
@@ -1100,9 +1099,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
-version = "2.1.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
dependencies = [
"icu_collections",
"icu_locale_core",
@@ -1114,9 +1113,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.1.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
[[package]]
name = "icu_provider"
@@ -1213,9 +1212,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1249,9 +1248,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.177"
+version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libm"
@@ -1288,9 +1287,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.28"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru-slab"
@@ -1337,9 +1336,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
@@ -1455,16 +1454,16 @@ dependencies = [
[[package]]
name = "oauth2-test-server"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bb78cf155f91eba1d99533e49aafc31f5e7e42b9964d2c0c8470d6641accb54"
+checksum = "e66b9483c4680a03f8f3a414e02d9e2b2d12702946d2fd05d58c3da4406630d2"
dependencies = [
"axum",
"base64 0.21.7",
"chrono",
"colored",
"futures",
- "http 1.3.1",
+ "http 1.4.0",
"jsonwebtoken",
"once_cell",
"rand 0.8.5",
@@ -1893,9 +1892,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
-version = "0.12.24"
+version = "0.12.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1905,7 +1904,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.4.12",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -1931,7 +1930,7 @@ dependencies = [
"tokio-rustls",
"tokio-util",
"tower",
- "tower-http 0.6.6",
+ "tower-http 0.6.8",
"tower-service",
"url",
"wasm-bindgen",
@@ -1989,7 +1988,7 @@ dependencies = [
"async-trait",
"base64 0.22.1",
"bytes",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"nanoid",
@@ -2021,9 +2020,9 @@ dependencies = [
[[package]]
name = "rust-mcp-schema"
-version = "0.7.5"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba217e6fcb043bba9e194209bff92c35294093187504d1443832ca2051816753"
+checksum = "8b6cf84194ba1c1703c7ad0a6730b483f1a34dd32057e8e7226387da3f876591"
dependencies = [
"serde",
"serde_json",
@@ -2039,7 +2038,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"futures",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -2124,9 +2123,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.13.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"web-time",
"zeroize",
@@ -2375,6 +2374,8 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"tokio",
+ "tracing",
+ "tracing-subscriber",
]
[[package]]
@@ -2761,7 +2762,7 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
"bitflags",
"bytes",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"pin-project-lite",
@@ -2772,14 +2773,14 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.6"
+version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-util",
- "http 1.3.1",
+ "http 1.4.0",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
@@ -2802,9 +2803,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -2814,9 +2815,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -2825,9 +2826,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -2846,9 +2847,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.20"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -2918,9 +2919,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.18.1"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.3.4",
"js-sys",
@@ -2983,9 +2984,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
@@ -2996,9 +2997,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.55"
+version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
"js-sys",
@@ -3009,9 +3010,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3019,9 +3020,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3032,9 +3033,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
@@ -3054,9 +3055,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3375,18 +3376,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.28"
+version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90"
+checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.28"
+version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26"
+checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index 3b7f98c..cd0dcc1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,8 +30,7 @@ rust-mcp-macros = { version = "0.5.3", path = "crates/rust-mcp-macros", default-
rust-mcp-extra = { version="0.1.0", path = "crates/rust-mcp-extra", default-features = false }
# External crates
-rust-mcp-schema = { version = "0.7", default-features = false }
-
+rust-mcp-schema = { version="0.9", default-features = false }
futures = { version = "0.3" }
tokio = { version = "1.4", features = ["full"] }
diff --git a/README.md b/README.md
index d92d964..715f280 100644
--- a/README.md
+++ b/README.md
@@ -11,26 +11,21 @@
[
](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!
-**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem.
-Leveraging the [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) crate simplifies the process of building robust and reliable MCP servers and clients, ensuring consistency and minimizing errors in data handling and message processing.
+A high-performance, asynchronous Rust toolkit for building MCP servers and clients.
+Focus on your application logic - rust-mcp-sdk handles the protocol, transports, and the rest!
+This SDK fully implements the latest MCP protocol version ([2025-11-25](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema)), with backward compatibility built-in. `rust-mcp-sdk` provides the necessary components for developing both servers and clients in the MCP ecosystem. It leverages the [rust-mcp-schema](https://crates.io/crates/rust-mcp-schema) crate for type-safe schema objects and includes powerful procedural macros for tools and user input elicitation.
-**rust-mcp-sdk** supports all three official versions of the MCP protocol.
-By default, it uses the **2025-06-18** version, but earlier versions can be enabled via Cargo features.
-
-π 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.
-
-
-**Features**
-- β
Stdio, SSE and Streamable HTTP Support
-- β
Supports multiple MCP protocol versions
+**Key Features**
+- β
Latest MCP protocol specification supported: 2025-11-25
+- β
Transports:Stdio, Streamable HTTP, and backward-compatible SSE support
+- β
Lightweight Axum-based server for Streamable HTTP and SSE
+- β
Multi-client concurrency
- β
DNS Rebinding Protection
+- β
Resumability
- β
Batch Messages
- β
Streaming & non-streaming JSON response
-- β
Resumability
- β
OAuth Authentication for MCP Servers
- β
[Remote Oauth Provider](crates/rust-mcp-sdk/src/auth/auth_provider/remote_auth_provider.rs) (for any provider with DCR support)
- β
**Keycloak** Provider (via [rust-mcp-extra](crates/rust-mcp-extra/README.md#keycloak))
@@ -41,24 +36,26 @@ By default, it uses the **2025-06-18** version, but earlier versions can be enab
**β οΈ** Project is currently under development and should be used at your own risk.
## Table of Contents
-- [Getting Started](#getting-started)
+- [Quick Start](#quick-start)
+ - [Minimal MCP Server (Stdio)]([#minimal-mcp-server-stdio](#minimal-mcp-server-stdio))
+ - [Minimal MCP Server (Streamable HTTP)](#minimal-mcp-server-streamable-http)
+ - [Minimal MCP Client (Stdio)](#minimal-mcp-client-stdio)
- [Usage Examples](#usage-examples)
- - [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)
-- [Authentication](#authentication)
- [Macros](#macros)
+ - [mcp_tool](#mcp_tool)
+ - [tool_box](#-tool_box)
+ - [mcp_icon](#-mcp_icon)
+- [Authentication](#authentication)
+ - [RemoteAuthProvider](#remoteauthprovider)
+ - [OAuthProxy](#oauthproxy)
- [HyperServerOptions](#hyperserveroptions)
- - [Security Considerations](#security-considerations)
+- [Security Considerations](#security-considerations)
- [Cargo features](#cargo-features)
- [Available Features](#available-features)
- - [MCP protocol versions with corresponding features](#mcp-protocol-versions-with-corresponding-features)
- [Default Features](#default-features)
- [Using Only the server Features](#using-only-the-server-features)
- [Using Only the client Features](#using-only-the-client-features)
-- [Choosing Between Standard and Core Handlers traits](#choosing-between-standard-and-core-handlers-traits)
+- [Handler Traits](#handlers-traits)
- [Choosing Between **ServerHandler** and **ServerHandlerCore**](#choosing-between-serverhandler-and-serverhandlercore)
- [Choosing Between **ClientHandler** and **ClientHandlerCore**](#choosing-between-clienthandler-and-clienthandlercore)
- [Projects using Rust MCP SDK](#projects-using-rust-mcp-sdk)
@@ -67,330 +64,339 @@ By default, it uses the **2025-06-18** version, but earlier versions can be enab
- [License](#license)
-## Getting Started
-If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md)
-## Usage Examples
+## Quick Start
-### MCP Server (stdio)
+
-Create a MCP server with a `tool` that will print a `Hello World!` message:
+Add to your Cargo.toml:
+```toml
+[dependencies]
+rust-mcp-sdk = "0.9.0" # Check crates.io for the latest version
+```
+
+
+
+## Minimal MCP Server (Stdio)
+```rs
+use async_trait::async_trait;
+use rust_mcp_sdk::{*,error::SdkResult,macros,mcp_server::{server_runtime, ServerHandler},schema::*,};
+
+// Define a mcp tool
+#[macros::mcp_tool(name = "say_hello", description = "returns \"Hello from Rust MCP SDK!\" message ")]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
+pub struct SayHelloTool {}
+
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler;
+
+// implement ServerHandler
+#[async_trait]
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {
+ tools: vec![SayHelloTool::tool()],
+ meta: None,
+ next_cursor: None,
+ })
+ }
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(&self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {
+ Ok(CallToolResult::text_content(vec!["Hello from Rust MCP SDK!".into()]))
+ } else {
+ Err(CallToolError::unknown_tool(params.name))
+ }
+ }
+}
-```rust
#[tokio::main]
async fn main() -> SdkResult<()> {
-
- // STEP 1: Define server details and capabilities
- let server_details = InitializeResult {
- // server name and version
+ // Define server details and capabilities
+ let server_info = InitializeResult {
server_info: Implementation {
- name: "Hello World MCP Server".to_string(),
- version: "0.1.0".to_string(),
- title: Some("Hello World MCP Server".to_string()),
- },
- capabilities: ServerCapabilities {
- // indicates that server support mcp tools
- tools: Some(ServerCapabilitiesTools { list_changed: None }),
- ..Default::default() // Using default values for other fields
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
},
- meta: None,
- instructions: Some("server instructions...".to_string()),
- protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
};
- // STEP 2: create a std transport with default options
let transport = StdioTransport::new(TransportOptions::default())?;
-
- // STEP 3: instantiate our custom handler for handling MCP messages
- let handler = MyServerHandler {};
-
- // STEP 4: create a MCP server
- let server: ServerRuntime = server_runtime::create_server(server_details, transport, handler);
-
- // STEP 5: Start the server
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = server_runtime::create_server(server_info, transport, handler);
server.start().await
-
}
```
-See hello-world-mcp-server-stdio example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
-
-
-
-### MCP Server (Streamable HTTP)
-
-Creating an MCP server in `rust-mcp-sdk` with the `sse` transport allows multiple clients to connect simultaneously with no additional setup.
-Simply create a Hyper Server using `hyper_server::create_server()` and pass in the same handler and HyperServerOptions.
-
-
-π‘ By default, both **Streamable HTTP** and **SSE** transports are enabled for backward compatibility. To disable the SSE transport , set the `sse_support` to false in the `HyperServerOptions`.
-
+## Minimal MCP Server (Streamable HTTP)
+Creating an MCP server in `rust-mcp-sdk` allows multiple clients to connect simultaneously with no additional setup.
+The setup is nearly identical to the stdio example shown above. You only need to create a Hyper server via `hyper_server::create_server()` and pass in the same handler and `HyperServerOptions`.
+π‘ If backward compatibility is required, you can enable **SSE** transport by setting `sse_support` to true in `HyperServerOptions`.
```rust
-
-// STEP 1: Define server details and capabilities
-let server_details = InitializeResult {
- // server name and version
- server_info: Implementation {
- name: "Hello World MCP Server".to_string(),
- version: "0.1.0".to_string(),
- title: Some("Hello World MCP Server".to_string()),
- },
- capabilities: ServerCapabilities {
- // indicates that server support mcp tools
- tools: Some(ServerCapabilitiesTools { list_changed: None }),
- ..Default::default() // Using default values for other fields
- },
- meta: None,
- instructions: Some("server instructions...".to_string()),
- protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
+use async_trait::async_trait;
+use rust_mcp_sdk::{*,error::SdkResult,event_store::InMemoryEventStore,macros,
+ mcp_server::{hyper_server, HyperServerOptions, ServerHandler},schema::*,
};
-// STEP 2: instantiate our custom handler for handling MCP messages
-let handler = MyServerHandler {};
-
-// STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions
-let server = hyper_server::create_server(
- server_details,
- handler,
- HyperServerOptions {
- host: "127.0.0.1".to_string(),
- sse_support: false,
- event_store: Some(Arc::new(InMemoryEventStore::default())), // enable resumability
- ..Default::default()
- },
-);
-
-// STEP 4: Start the server
-server.start().await?;
-
-Ok(())
-```
-
-
-The implementation of `MyServerHandler` is the same regardless of the transport used and could be as simple as the following:
-
-```rust
-
-// STEP 1: Define a rust_mcp_schema::Tool ( we need one with no parameters for this example)
-#[mcp_tool(name = "say_hello_world", description = "Prints \"Hello World!\" message")]
-#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+// Define a mcp tool
+#[macros::mcp_tool(
+ name = "say_hello",
+ description = "returns \"Hello from Rust MCP SDK!\" message "
+)]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
pub struct SayHelloTool {}
-// STEP 2: Implement ServerHandler trait for a custom handler
-// For this example , we only need handle_list_tools_request() and handle_call_tool_request() methods.
-pub struct MyServerHandler;
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler;
+// implement ServerHandler
#[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: Arc) -> Result {
-
- Ok(ListToolsResult {
- tools: vec![SayHelloTool::tool()],
- meta: None,
- next_cursor: None,
- })
-
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {tools: vec![SayHelloTool::tool()],meta: None,next_cursor: None})
}
-
- /// Handles requests to call a specific tool.
- 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())] ))
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(
+ &self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {Ok(CallToolResult::text_content(vec!["Hello from Rust MCP SDK!".into()]))
} else {
- Err(CallToolError::unknown_tool(request.tool_name().to_string()))
+ Err(CallToolError::unknown_tool(params.name))
}
-
}
}
-```
-
----
-
-π 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) :
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+ // Define server details and capabilities
+ let server_info = InitializeResult {
+ server_info: Implementation {
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
+ },
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
+ };
-
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = hyper_server::create_server(
+ server_info,
+ handler,
+ HyperServerOptions {
+ host: "127.0.0.1".to_string(),
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
+ ..Default::default()
+ },
+ );
+ server.start().await?;
+ Ok(())
+}
+```
----
-### MCP Client (stdio)
+## Minimal MCP Client (Stdio)
+Following is implementation of an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools provided by the server.
-Create an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools, then uses the add tool provided by the server to sum 120 and 28, printing the result.
```rust
+use async_trait::async_trait;
+use rust_mcp_sdk::{*, error::SdkResult,
+ mcp_client::{client_runtime, ClientHandler},
+ schema::*,
+};
-// STEP 1: Custom Handler to handle incoming MCP Messages
+// 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
+ // To see all the trait methods you can override,
+ // check out:
+ // 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
+ // Client details and capabilities
let client_details: InitializeRequestParams = InitializeRequestParams {
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "simple-rust-mcp-client".into(),
version: "0.1.0".into(),
+ description: None,
+ icons: vec![],
+ title: None,
+ website_url: None,
},
- protocol_version: LATEST_PROTOCOL_VERSION.into(),
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ meta: None,
};
- // Step3 : Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server
+ // Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server
let transport = StdioTransport::create_with_server_launch(
- "npx",
- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()],
- None, TransportOptions::default()
+ "npx",vec!["-y".to_string(),"@modelcontextprotocol/server-everything@latest".to_string()],
+ None,
+ TransportOptions::default(),
)?;
- // STEP 4: instantiate our custom handler for handling MCP messages
+ // instantiate our custom handler for handling MCP messages
let handler = MyClientHandler {};
- // STEP 5: create a MCP client
- let client = client_runtime::create_client(client_details, transport, handler);
-
- // STEP 6: start the MCP client
+ // Create and start the MCP client
+ let client = client_runtime::create_client(client_details, transport, handler);
client.clone().start().await?;
+ // use client methods to communicate with the MCP Server as you wish:
- // STEP 7: use client methods to communicate with the MCP Server as you wish
-
+ let server_version = client.server_version().unwrap();
+
// 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);
-
+ let tools = client.request_tool_list(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!(" {}. {} : {}", 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(())
}
-
```
-Here is the output :
+## Usage Examples
-
+π For full examples (stdio, Streamable HTTP, clients, auth, etc.), see the [examples/](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples) directory.
-> your results may vary slightly depending on the version of the MCP Server in use when you run it.
+π If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md)
-### MCP Client (Streamable HTTP)
-```rs
+See [hello-world-mcp-server-stdio](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio) example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
-// 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<()> {
+## Macros
+Enable with the `macros` feature.
- // 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(),
- };
+[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.
- // 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()
- },
- };
+### βΎ`mcp_tool`
+Generate a [Tool](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.Tool.html) from a struct, with rich metadata (icons, execution hints, etc.).
- // STEP 4: instantiate the custom handler that is responsible for handling MCP messages
- let handler = MyClientHandler {};
+example usage:
+```rs
+#[mcp_tool(
+ name = "write_file",
+ title = "Write File Tool",
+ description = "Create a new file or completely overwrite an existing file with new content.",
+ destructive_hint = false idempotent_hint = false open_world_hint = false read_only_hint = false,
+ meta = r#"{ "key" : "value", "string_meta" : "meta value", "numeric_meta" : 15}"#,
+ execution(task_support = "optional"),
+ icons = [(src = "https:/website.com/write.png", mime_type = "image/png", sizes = ["128x128"], theme = "light")]
+)]
+#[derive(rust_mcp_macros::JsonSchema)]
+pub struct WriteFileTool {
+ /// The target file's path for writing content.
+ pub path: String,
+ /// The string content to be written to the file
+ pub content: String,
+}
+```
- // STEP 5: create the client with transport options and the handler
- let client = client_runtime::with_transport_options(client_details, transport_options, handler);
+π For complete documentation, example usage, and a list of all available attributes, please refer to https://crates.io/crates/rust-mcp-macros.
- // STEP 6: start the MCP client
- client.clone().start().await?;
+### βΎ `tool_box!()`
+Automatically generates an enum based on the provided list of tools, making it easier to organize and manage them, especially when your application includes a large number of tools.
- // STEP 7: use client methods to communicate with the MCP Server as you wish
+```rs
+tool_box!(GreetingTools, [SayHelloTool, SayGoodbyeTool]);
- // 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);
+let tools: Vec = GreetingTools::tools();
+``
- tools.iter().enumerate().for_each(|(tool_index, tool)| {
- println!(" {}. {} : {}",
- tool_index + 1,
- tool.name,
- tool.description.clone().unwrap_or_default()
- );
- });
+π» For a real-world example, check out [tools/](https://github.com/rust-mcp-stack/rust-mcp-filesystem/tree/main/src/tools) and
+[handle_call_tool_request(...)](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L195) in [rust-mcp-filesystem](https://github.com/rust-mcp-stack/rust-mcp-filesystem) project
- 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)};
+### βΎ [mcp_elicit](https://crates.io/crates/rust-mcp-macros)
+Generates type-safe elicitation (Form or URL mode) for user input.
- // invoke the tool
- let result = client.call_tool(request).await?;
+example usage:
+```rs
+#[mcp_elicit(message = "Please enter your info", mode = form)]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
+}
- println!("{}",result.content.first().unwrap().as_text_content()?.text);
+// Sends a request to the client asking the user to provide input
+let result: ElicitResult = server.request_elicitation(UserInfo::elicit_request_params()).await?;
- client.shut_down().await?;
+// Convert result.content into a UserInfo instance
+let user_info = UserInfo::from_elicit_result_content(result.content)?;
- Ok(())
+println!("name: {}", user_info.name);
+println!("age: {}", user_info.age);
+println!("email: {}",user.email.clone().unwrap_or("not provider".into()));
+println!("tags: {}", user_info.tags.join(","));
```
-π 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.
+π For complete documentation, example usage, and a list of all available attributes, please refer to https://crates.io/crates/rust-mcp-macros.
+### βΎ `mcp_icon!()`
+A convenient icon builder for implementations and tools, offering full attribute support including theme, size, mime, and more.
-### MCP Client (sse)
-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(
-- "npx",
-- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()],
-- None, TransportOptions::default()
--)?;
-+ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?;
+example usage:
+```rs
+let icon: crate::schema::Icon = mcp_icon!(
+ src = "http://website.com/icon.png",
+ mime_type = "image/png",
+ sizes = ["64x64"],
+ theme = "dark"
+ );
```
-π 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.
-
-
## Authentication
MCP server can verify tokens issued by other systems, integrate with external identity providers, or manage the entire authentication process itself. Each option offers a different balance of simplicity, security, and control.
@@ -404,120 +410,12 @@ MCP server can verify tokens issued by other systems, integrate with external id
- [WorkOS autn example](crates/rust-mcp-extra/README.md#workos-authkit)
-
### OAuthProxy
OAuthProxy enables authentication with OAuth providers that donβt support Dynamic Client Registration (DCR).It accepts any client registration request, handles the DCR on your server side and then uses your pre-registered app credentials upstream.The proxy also forwards callbacks, allowing dynamic redirect URIs to work with providers that require fixed ones.
> β οΈ OAuthProxy support is still in development, please use RemoteAuthProvider for now.
-
-
-## 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
## HyperServerOptions
@@ -531,89 +429,22 @@ A typical example of creating a HyperServer that exposes the MCP server via Stre
let server = hyper_server::create_server(
server_details,
- handler,
+ handler.to_mcp_server_handler(),
HyperServerOptions {
host: "127.0.0.1".to_string(),
- enable_ssl: true,
+ port: 8080,
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
+ auth: Some(Arc::new(auth_provider)), // enable authentication
+ sse_support: false,
..Default::default()
},
);
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,
-
- /// 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.
- /// Default is false (SSE streams are preferred).
- pub enable_json_response: Option,
-
- /// Interval between automatic ping messages sent to clients to detect disconnects
- pub ping_interval: Duration,
-
- /// Enables SSL/TLS if set to `true`
- pub enable_ssl: bool,
-
- /// Path to the SSL/TLS certificate file (e.g., "cert.pem").
- /// Required if `enable_ssl` is `true`.
- pub ssl_cert_path: Option,
-
- /// Path to the SSL/TLS private key file (e.g., "key.pem").
- /// Required if `enable_ssl` is `true`.
- pub ssl_key_path: Option,
+π Refer to [HyperServerOptions](https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/hyper_servers/server.rs#L43) for a complete overview of HyperServerOptions attributes and options.
- /// List of allowed host header values for DNS rebinding protection.
- /// If not specified, host validation is disabled.
- pub allowed_hosts: Option>,
-
- /// List of allowed origin header values for DNS rebinding protection.
- /// If not specified, origin validation is disabled.
- pub allowed_origins: Option>,
-
- /// 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,
-
- /// Optional authentication provider for protecting MCP server.
- pub auth: Option>,
-}
-
-```
### Security Considerations
@@ -637,28 +468,19 @@ The `rust-mcp-sdk` crate provides several features that can be enabled or disabl
- `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
-
-- `2025_06_18` : Activates MCP Protocol version 2025-06-18 (enabled by default)
-- `2025_03_26` : Activates MCP Protocol version 2025-03-26
-- `2024_11_05` : Activates MCP Protocol version 2024-11-05
-
-> Note: MCP protocol versions are mutually exclusive-only one can be active at any given time.
-
### Default Features
-When you add rust-mcp-sdk as a dependency without specifying any features, all features are included, with the latest MCP Protocol version enabled by default:
+When you add rust-mcp-sdk as a dependency without specifying any features, all features are enabled by default
```toml
[dependencies]
-rust-mcp-sdk = "0.2.0"
+rust-mcp-sdk = "0.9.0"
```
diff --git a/assets/rust-mcp-icon.png b/assets/rust-mcp-icon.png
new file mode 100644
index 0000000..189ea43
Binary files /dev/null and b/assets/rust-mcp-icon.png differ
diff --git a/crates/rust-mcp-extra/Cargo.toml b/crates/rust-mcp-extra/Cargo.toml
index 3c3b438..e61a4e2 100644
--- a/crates/rust-mcp-extra/Cargo.toml
+++ b/crates/rust-mcp-extra/Cargo.toml
@@ -13,7 +13,7 @@ rust-version = { workspace = true }
exclude = ["assets/", "tests/"]
[dependencies]
-rust-mcp-sdk = { version = "0.7.4" , path = "../rust-mcp-sdk", default-features = false, features=["server","2025_06_18","auth","hyper-server","macros"] }
+rust-mcp-sdk = { version = "0.7.4" , path = "../rust-mcp-sdk", default-features = false, features=["server","auth","hyper-server","macros"] }
base64 = {workspace = true, optional=true}
url= {workspace = true, optional=true}
nanoid = {version="0.4", optional=true}
diff --git a/crates/rust-mcp-extra/examples/common/handler.rs b/crates/rust-mcp-extra/examples/common/handler.rs
index 5fc0714..8f8f8fd 100644
--- a/crates/rust-mcp-extra/examples/common/handler.rs
+++ b/crates/rust-mcp-extra/examples/common/handler.rs
@@ -1,16 +1,14 @@
-use std::sync::Arc;
-
+use crate::common::tool::ShowAuthInfo;
use async_trait::async_trait;
use rust_mcp_sdk::{
mcp_server::ServerHandler,
schema::{
- schema_utils::CallToolError, CallToolRequest, CallToolResult, ListToolsRequest,
- ListToolsResult, RpcError,
+ schema_utils::CallToolError, CallToolRequestParams, CallToolResult, ListToolsResult,
+ PaginatedRequestParams, RpcError,
},
McpServer,
};
-
-use crate::common::tool::ShowAuthInfo;
+use std::sync::Arc;
pub struct McpServerHandler;
#[async_trait]
@@ -18,7 +16,7 @@ impl ServerHandler for McpServerHandler {
// Handle ListToolsRequest, return list of available tools as ListToolsResult
async fn handle_list_tools_request(
&self,
- _request: ListToolsRequest,
+ _request: Option,
_runtime: Arc,
) -> std::result::Result {
Ok(ListToolsResult {
@@ -31,16 +29,16 @@ impl ServerHandler for McpServerHandler {
/// Handles incoming CallToolRequest and processes it using the appropriate tool.
async fn handle_call_tool_request(
&self,
- request: CallToolRequest,
+ params: CallToolRequestParams,
runtime: Arc,
) -> std::result::Result {
- if request.params.name.eq(&ShowAuthInfo::tool_name()) {
+ if params.name.eq(&ShowAuthInfo::tool_name()) {
let tool = ShowAuthInfo::default();
tool.call_tool(runtime.auth_info_cloned().await)
} else {
Err(CallToolError::from_message(format!(
"Tool \"{}\" does not exists or inactive!",
- request.params.name,
+ params.name,
)))
}
}
diff --git a/crates/rust-mcp-extra/examples/common/utils.rs b/crates/rust-mcp-extra/examples/common/utils.rs
index 6889b56..5091b31 100644
--- a/crates/rust-mcp-extra/examples/common/utils.rs
+++ b/crates/rust-mcp-extra/examples/common/utils.rs
@@ -1,6 +1,9 @@
-use rust_mcp_sdk::schema::{
- Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools,
- LATEST_PROTOCOL_VERSION,
+use rust_mcp_sdk::{
+ mcp_icon,
+ schema::{
+ Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools,
+ LATEST_PROTOCOL_VERSION,
+ },
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@@ -10,6 +13,16 @@ pub fn create_server_info(server_name: &str) -> InitializeResult {
name: server_name.to_string(),
version: "0.1.0".to_string(),
title: Some(server_name.to_string()),
+ description: Some(server_name.to_string()),
+ icons: vec![
+ mcp_icon!(
+ src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "dark"
+ )
+ ],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".to_string()),
},
capabilities: ServerCapabilities {
tools: Some(ServerCapabilitiesTools { list_changed: None }),
diff --git a/crates/rust-mcp-extra/examples/keycloak-auth.rs b/crates/rust-mcp-extra/examples/keycloak-auth.rs
index b4b191b..58bd9e1 100644
--- a/crates/rust-mcp-extra/examples/keycloak-auth.rs
+++ b/crates/rust-mcp-extra/examples/keycloak-auth.rs
@@ -7,6 +7,7 @@ use rust_mcp_extra::auth_provider::keycloak::{KeycloakAuthOptions, KeycloakAuthP
use rust_mcp_sdk::{
error::SdkResult,
mcp_server::{hyper_server, HyperServerOptions},
+ ToMcpServerHandler,
};
use std::{env, sync::Arc};
@@ -31,7 +32,7 @@ async fn main() -> SdkResult<()> {
let server = hyper_server::create_server(
server_details,
- handler,
+ handler.to_mcp_server_handler(),
HyperServerOptions {
host: "localhost".to_string(),
port: 3000,
diff --git a/crates/rust-mcp-extra/examples/scalekit-auth.rs b/crates/rust-mcp-extra/examples/scalekit-auth.rs
index 8fd625f..cf76efb 100644
--- a/crates/rust-mcp-extra/examples/scalekit-auth.rs
+++ b/crates/rust-mcp-extra/examples/scalekit-auth.rs
@@ -6,7 +6,9 @@ use crate::common::{
use rust_mcp_extra::auth_provider::scalekit::{ScalekitAuthOptions, ScalekitAuthProvider};
use rust_mcp_sdk::{
error::SdkResult,
+ event_store::InMemoryEventStore,
mcp_server::{hyper_server, HyperServerOptions},
+ ToMcpServerHandler,
};
use std::{env, sync::Arc};
@@ -32,10 +34,11 @@ async fn main() -> SdkResult<()> {
let server = hyper_server::create_server(
server_details,
- handler,
+ handler.to_mcp_server_handler(),
HyperServerOptions {
host: "127.0.0.1".to_string(),
- port: 3000,
+ port: 8080,
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
auth: Some(Arc::new(auth_provider)), // enable authentication
sse_support: false,
..Default::default()
diff --git a/crates/rust-mcp-extra/examples/workos-auth.rs b/crates/rust-mcp-extra/examples/workos-auth.rs
index 01d980b..c948e90 100644
--- a/crates/rust-mcp-extra/examples/workos-auth.rs
+++ b/crates/rust-mcp-extra/examples/workos-auth.rs
@@ -7,6 +7,7 @@ use rust_mcp_extra::auth_provider::work_os::{WorkOSAuthOptions, WorkOsAuthProvid
use rust_mcp_sdk::{
error::SdkResult,
mcp_server::{hyper_server, HyperServerOptions},
+ ToMcpServerHandler,
};
use std::{env, sync::Arc};
@@ -29,7 +30,7 @@ async fn main() -> SdkResult<()> {
let server = hyper_server::create_server(
server_details,
- handler,
+ handler.to_mcp_server_handler(),
HyperServerOptions {
host: "127.0.0.1".to_string(),
port: 3000,
diff --git a/crates/rust-mcp-macros/Cargo.toml b/crates/rust-mcp-macros/Cargo.toml
index 58490d4..424e014 100644
--- a/crates/rust-mcp-macros/Cargo.toml
+++ b/crates/rust-mcp-macros/Cargo.toml
@@ -18,13 +18,12 @@ description = "A procedural macro, part of the rust-mcp-sdk ecosystem, that deri
[dependencies]
serde_json = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
-syn = "2.0"
+syn = {version="2.0", features = ["full", "extra-traits","parsing"]}
quote = "1.0"
proc-macro2 = "1.0"
-
[dev-dependencies]
-rust-mcp-schema = { workspace = true, default-features = false }
+rust-mcp-schema = { workspace = true , features=["latest","schema_utils"]}
[lints]
workspace = true
@@ -32,18 +31,7 @@ workspace = true
[lib]
proc-macro = true
-
[features]
# defalt features
-default = ["2025_06_18"] # Default features
-
-# activates the latest MCP schema version, this will be updated once a new version of schema is published
-latest = ["2025_06_18"]
-
-# enables mcp schema version 2025_06_18
-2025_06_18 = ["rust-mcp-schema/2025_06_18", "rust-mcp-schema/schema_utils"]
-# enables mcp schema version 2025_03_26
-2025_03_26 = ["rust-mcp-schema/2025_03_26", "rust-mcp-schema/schema_utils"]
-# enables mcp schema version 2024_11_05
-2024_11_05 = ["rust-mcp-schema/2024_11_05", "rust-mcp-schema/schema_utils"]
+default = []
sdk = []
diff --git a/crates/rust-mcp-macros/README.md b/crates/rust-mcp-macros/README.md
index fc463cd..57b000d 100644
--- a/crates/rust-mcp-macros/README.md
+++ b/crates/rust-mcp-macros/README.md
@@ -1,195 +1,186 @@
-# rust-mcp-macros.
+# rust-mcp-macros
+
+`rust-mcp-macros` provides procedural macros for the [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) ecosystem. These macros simplify the generation of `tools` and `elicitation` schemas compatible with the latest MCP protocol specifications.
+
+
+The available macros are:
+
+[mcp_tool](#mcp_tool-macro): Generates a [rust_mcp_schema::Tool](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.Tool.html) instance from a struct.
+[mcp_elicit](#mcp_elicit): Generates elicitation logic for gathering user input based on a struct's schema, supporting [Form](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.ElicitRequestFormParams.html) and [URL](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.ElicitRequestUrlParams.html) modes.
+[derive(JsonSchema)]: Derives a JSON Schema representation for structs and enums, used by the other macros for schema generation.
+
+These macros rely on [rust_mcp_schema](https://crates.io/crates/rust-mcp-schema) and serde_json for schema handling.
## mcp_tool Macro
+A procedural macro to generate a [rust_mcp_schema::Tool](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.Tool.html) instance from a struct. The struct must derive **JsonSchema**.
+
-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.
+### Generated methods:
-The `mcp_tool` macro generates an implementation for the annotated struct that includes:
+- `tool_name()`: Returns the tool's name.
+- `tool()`: Returns a [rust_mcp_schema::Tool](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.Tool.html) with name, description, input schema, and optional metadata/annotations.
+- `request_params()`: Returns a [CallToolRequestParams](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.CallToolRequestParams.html) pre-initialized with the tool's name, ready for building a tool call via the builder pattern.
-- A `tool_name()` method returning the tool's name as a string.
-- A `tool()` method returning a `rust_mcp_schema::Tool` instance with the tool's name,
- description, and input schema derived from the struct's fields.
-## Attributes
+### Attributes
-- `name` - The name of the tool (required, non-empty string).
+- `name`: Required, non-empty string for the tool's name.
+- `description`: Required, a full and detailed description of the toolβs functionality.
+- `title`: Optional human readable title for the tools.
- `description` - A description of the tool (required, non-empty string).
-- `title` - An optional human-readable and easily understood title.
- `meta` - An optional JSON string that provides additional metadata for the tool.
+- `execution`: Optional, controls task support. Accepted values are "required", "optional", and "forbidden".
+- `icons`: Optional array of icons with src (required), mime_type, sizes (array of strings), theme ("light" or "dark").
- `destructive_hint` β Optional boolean, indicates whether the tool may make destructive changes to its environment.
- `idempotent_hint` β Optional boolean, indicates whether repeated calls with the same input have the same effect.
- `open_world_hint` β Optional boolean, indicates whether the tool can interact with external or unknown entities.
- `read_only_hint` β Optional boolean, indicates whether the tool makes no modifications to its environment.
-
-## Usage Example
+### Usage Example
```rust
+use rust_mcp_macros::{mcp_tool, JsonSchema};
+use rust_mcp_schema::Tool;
#[mcp_tool(
- name = "write_file",
- title = "Write File Tool"
- description = "Create a new file or completely overwrite an existing file with new content."
- destructive_hint = false
- idempotent_hint = false
- open_world_hint = false
- read_only_hint = false
- meta = r#"{
- "key" : "value",
- "string_meta" : "meta value",
- "numeric_meta" : 15
- }"#
+ name = "write_file",
+ title = "Write File Tool",
+ description = "Create or overwrite a file with content.",
+ destructive_hint = false,
+ idempotent_hint = false,
+ open_world_hint = false,
+ read_only_hint = false,
+ execution(task_support = "optional"),
+ icons = [
+ (src = "https:/mywebsite.com/write.png", mime_type = "image/png", sizes = ["128x128"], theme = "light"),
+ (src = "https:/mywebsite.com/write_dark.svg", mime_type = "image/svg+xml", sizes = ["64x64","128x128"], theme = "dark")
+ ],
+ meta = r#"{"key": "value"}"#
)]
-#[derive(rust_mcp_macros::JsonSchema)]
+#[derive(JsonSchema)]
pub struct WriteFileTool {
- /// The target file's path for writing content.
+ /// The target file's path.
pub path: String,
/// The string content to be written to the file
pub content: String,
}
-fn main() {
-
- assert_eq!(WriteFileTool::tool_name(), "write_file");
-
- let tool: rust_mcp_schema::Tool = WriteFileTool::tool();
- assert_eq!(tool.name, "write_file");
- assert_eq!(tool.title.as_ref().unwrap(), "Write File Tool");
- assert_eq!( tool.description.unwrap(),"Create a new file or completely overwrite an existing file with new content.");
-
- let meta: &Map = tool.meta.as_ref().unwrap();
- assert_eq!(
- meta.get("key").unwrap(),
- &Value::String("value".to_string())
- );
-
- let schema_properties = tool.input_schema.properties.unwrap();
- assert_eq!(schema_properties.len(), 2);
- assert!(schema_properties.contains_key("path"));
- assert!(schema_properties.contains_key("content"));
-
- // get the `content` prop from schema
- let content_prop = schema_properties.get("content").unwrap();
-
- // assert the type
- assert_eq!(content_prop.get("type").unwrap(), "string");
- // assert the description
- assert_eq!(
- content_prop.get("description").unwrap(),
- "The string content to be written to the file"
- );
+WriteFileTool::request_params().with_arguments(
+ json!({"path":"./test.txt","content":"hello tool"})
+ .as_object()
+ .unwrap()
+ .clone(),
+)
+
+// send a call_tool requeest:
+let result = client.request_tool_call( WriteFileTool::request_params().with_arguments(
+ json!({"path":"./test.txt","content":"hello tool"}).as_object().unwrap().clone(),
+))?;
+
+// Handle ListToolsRequest, return list of available tools as ListToolsResult
+async fn handle_list_tools_request(
+ &self,
+ request: Option,
+ runtime: Arc,
+) -> std::result::Result {
+ Ok(ListToolsResult {
+ meta: None,
+ next_cursor: None,
+ tools: vec![WriteFileTool::tool()],
+ })
}
```
+## mcp_elicit Macro
-
-**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).
-
-- `destructive_hint`
-- `idempotent_hint`
-- `open_world_hint`
-- `read_only_hint`
-
-
-
+The `mcp_elicit` macro generates implementations for eliciting user input based on the struct's schema. The struct must derive **JsonSchema**. It supports two modes: **form** (default) for schema-based forms and **url** for redirecting the user to an external URL to collect input.
-## mcp_elicit Macro
+### Generated methods:
-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:
+- `message()`: Returns the elicitation message.
+- `elicit_request_params(elicitation_id)`: Returns [ElicitRequestParams](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.ElicitRequestUrlParams.html) (FormParams or UrlParams based on mode).
+- `from_elicit_result_content(content)`: Parses user input back into the struct.
-- 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.
+- `message` : Optional string (or concat!(...)), defaults to empty.
+- `mode`: Optional, elicitation mode ("form"|"URL), defaults to form.
+- `url` = "https://example.com/form": Required if mode = url.
### 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.
+- `String`: Maps to [ElicitResultContentPrimitive::String](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/enum.ElicitResultContentPrimitive.html).
+- `bool`: Maps to [ElicitResultContentPrimitive::Boolean](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/enum.ElicitResultContentPrimitive.html).
+- `i32`: Maps to [ElicitResultContentPrimitive::Integer](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/enum.ElicitResultContentPrimitive.html) (with bounds checking).
+- `i64`: Maps to [ElicitResultContentPrimitive::Integer](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/enum.ElicitResultContentPrimitive.html).
+- `Vec`: Maps to [ElicitResultContent::StringArray](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/enum.ElicitResultContent.html).
- `Option`: Supported for any of the above types, mapping to `None` if the field is missing.
-### Usage Example
+### Usage Example (Form Mode)
```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())),
- }
+ #[mcp_elicit(message = "Please enter your info", mode = form)]
+ #[derive(JsonSchema)]
+ pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
}
-}
-// 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,
-}
+ // Sends a request to the client asking the user to provide input
+ let result: ElicitResult = server.request_elicitation(UserInfo::elicit_request_params()).await?;
- // ....
- // .......
- // ...........
+ // Convert result.content into a UserInfo instance
+ let user_info = UserInfo::from_elicit_result_content(result.content)?;
+
+ println!("name: {}", user_info.name);
+ println!("age: {}", user_info.age);
+ println!("email: {}",user.email.clone().unwrap_or("not provider".into()));
+ println!("tags: {}", user_info.tags.join(","));
- // 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)?;
+### Usage Example (URL Mode)
+```rust
+#[mcp_elicit(message = "Complete the form", mode = url, url = "https://example.com/form")]
+ #[derive(JsonSchema)]
+ pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
+ }
+ let elicit_url = UserInfo::elicit_url_params("elicit_10".into());
+
+ // Sends a request to the client asking the user to provide input
+ let result: ElicitResult = server.request_elicitation(UserInfo::elicit_request_params()).await?;
+
+ // Convert result.content into a UserInfo instance
+ let user_info = UserInfo::from_elicit_result_content(result.content)?;
+
+ println!("name: {}", user_info.name);
+ println!("age: {}", user_info.age);
+ println!("email: {}", user_info.email.unwrap_or_default();
+ println!("tags: {}", user_info.tags.join(","));
```
-
---
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/elicit.rs b/crates/rust-mcp-macros/src/elicit.rs
new file mode 100644
index 0000000..1ffdea9
--- /dev/null
+++ b/crates/rust-mcp-macros/src/elicit.rs
@@ -0,0 +1,2 @@
+pub(crate) mod generator;
+pub(crate) mod parser;
diff --git a/crates/rust-mcp-macros/src/elicit/generator.rs b/crates/rust-mcp-macros/src/elicit/generator.rs
new file mode 100644
index 0000000..fa72a4d
--- /dev/null
+++ b/crates/rust-mcp-macros/src/elicit/generator.rs
@@ -0,0 +1,189 @@
+use crate::is_option;
+use crate::is_vec_string;
+use quote::quote;
+use quote::ToTokens;
+use syn::{
+ punctuated::Punctuated, token::Comma, Expr, ExprLit, Ident, Lit, Meta, PathArguments, Token,
+ Type,
+};
+
+fn json_field_name(field: &syn::Field) -> String {
+ field
+ .attrs
+ .iter()
+ .filter(|a| a.path().is_ident("serde"))
+ .find_map(|attr| {
+ // Parse everything inside #[serde(...)]
+ let items = attr
+ .parse_args_with(Punctuated::::parse_terminated)
+ .ok()?;
+
+ for item in items {
+ match item {
+ // Case 1: #[serde(rename = "field_name")]
+ Meta::NameValue(nv) if nv.path.is_ident("rename") => {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) = nv.value
+ {
+ return Some(lit_str.value());
+ }
+ }
+
+ // Case 2: #[serde(rename(serialize = "a", deserialize = "b"))]
+ Meta::List(list) if list.path.is_ident("rename") => {
+ let inner_items = list
+ .parse_args_with(Punctuated::::parse_terminated)
+ .ok()?;
+
+ for inner in inner_items {
+ if let Meta::NameValue(nv) = inner {
+ if nv.path.is_ident("serialize") || nv.path.is_ident("deserialize")
+ {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) = nv.value
+ {
+ return Some(lit_str.value());
+ }
+ }
+ }
+ }
+ }
+
+ _ => {}
+ }
+ }
+ None
+ })
+ .unwrap_or_else(|| field.ident.as_ref().unwrap().to_string())
+}
+
+// Form implementation generation
+pub fn generate_from_impl(
+ fields: &Punctuated,
+ base: &proc_macro2::TokenStream,
+) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
+ let mut assigns = Vec::new();
+ let mut idents = Vec::new();
+
+ for field in fields {
+ let ident = field.ident.as_ref().unwrap();
+ let key = json_field_name(field);
+ let ty = &field.ty;
+
+ idents.push(ident);
+
+ let block = if is_option(ty) {
+ let inner = get_option_inner(ty);
+ let (expected, pat, conv) = match_type(inner, &key, base);
+ quote! {
+ let #ident = match map.remove(#key) {
+ Some(#pat) => Some(#conv),
+ Some(other) => return Err(RpcError::parse_error().with_message(format!(
+ "Type mismatch for optional field '{}': expected {}, got {:?}",
+ #key, #expected, other
+ ))),
+ None => None,
+ };
+ }
+ } else {
+ let (expected, pat, conv) = match_type(ty, &key, base);
+ quote! {
+ let #ident = match map.remove(#key) {
+ Some(#pat) => #conv,
+ Some(other) => return Err(RpcError::parse_error().with_message(format!(
+ "Type mismatch for required field '{}': expected {}, got {:?}",
+ #key, #expected, other
+ ))),
+ None => return Err(RpcError::parse_error().with_message(format!("Missing required field '{}'", #key))),
+ };
+ }
+ };
+
+ assigns.push(block);
+ }
+
+ (quote! { #(#assigns)* }, quote! { Self { #(#idents),* } })
+}
+
+pub fn get_option_inner(ty: &Type) -> &Type {
+ if let Type::Path(p) = ty {
+ if let Some(seg) = p.path.segments.last() {
+ if seg.ident == "Option" {
+ if let PathArguments::AngleBracketed(ref args) = seg.arguments {
+ if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
+ return inner;
+ }
+ }
+ }
+ }
+ }
+ panic!("Not Option")
+}
+
+pub fn match_type(
+ ty: &Type,
+ key: &str,
+ base: &proc_macro2::TokenStream,
+) -> (String, proc_macro2::TokenStream, proc_macro2::TokenStream) {
+ if is_vec_string(ty) {
+ return (
+ "string array".into(), // expected
+ quote! { V::StringArray(v) },
+ quote! { v },
+ );
+ };
+
+ match ty {
+ Type::Path(p) if p.path.is_ident("String") => (
+ "string".into(),
+ quote! { V::Primitive(#base::ElicitResultContentPrimitive::String(v)) },
+ quote! { v.clone() },
+ ),
+ Type::Path(p) if p.path.is_ident("bool") => (
+ "bool".into(),
+ quote! { V::Primitive(#base::ElicitResultContentPrimitive::Boolean(v)) },
+ quote! { v },
+ ),
+ Type::Path(p) if p.path.is_ident("i32") => (
+ "i32".into(),
+ quote! { V::Primitive(#base::ElicitResultContentPrimitive::Integer(v)) },
+ quote! { (v).try_into().map_err(|_| RpcError::parse_error().with_message(format!("i32 overflow in field '{}'", #key)))? },
+ ),
+ Type::Path(p) if p.path.is_ident("i64") => (
+ "i64".into(),
+ quote! { V::Primitive(#base::ElicitResultContentPrimitive::Integer(v)) },
+ quote! { v },
+ ),
+ _ => panic!("Unsupported type in mcp_elicit: {}", ty.to_token_stream()),
+ }
+}
+
+pub fn generate_form_schema(
+ struct_name: &Ident,
+ base: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ quote! {
+ {
+ let json = #struct_name::json_schema();
+ let properties = json.get("properties")
+ .and_then(|v| v.as_object())
+ .into_iter()
+ .flatten()
+ .filter_map(|(k, v)| #base::PrimitiveSchemaDefinition::try_from(v.as_object()?).ok().map(|def| (k.clone(), def)))
+ .collect();
+
+ let required = json.get("required")
+ .and_then(|v| v.as_array())
+ .into_iter()
+ .flatten()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect();
+
+ #base::ElicitFormSchema::new(properties, required, None)
+ }
+ }
+}
diff --git a/crates/rust-mcp-macros/src/elicit/parser.rs b/crates/rust-mcp-macros/src/elicit/parser.rs
new file mode 100644
index 0000000..fb1272a
--- /dev/null
+++ b/crates/rust-mcp-macros/src/elicit/parser.rs
@@ -0,0 +1,84 @@
+use syn::{punctuated::Punctuated, Expr, ExprLit, Lit, LitStr, Meta, Token};
+
+pub struct ElicitArgs {
+ pub message: LitStr,
+ pub mode: ElicitMode,
+}
+
+pub enum ElicitMode {
+ Form,
+ Url { url: LitStr },
+}
+
+impl syn::parse::Parse for ElicitArgs {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result {
+ let mut message = None;
+ let mut mode = ElicitMode::Form; // default
+ let mut url_lit: Option = None;
+
+ let metas = Punctuated::::parse_terminated(input)?;
+
+ // First pass
+ for meta in &metas {
+ if let Meta::NameValue(nv) = meta {
+ if let Some(ident) = nv.path.get_ident() {
+ if ident == "message" {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(s), ..
+ }) = &nv.value
+ {
+ message = Some(s.clone());
+ }
+ } else if ident == "url" {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(s), ..
+ }) = &nv.value
+ {
+ url_lit = Some(s.clone());
+ }
+ }
+ }
+ }
+ }
+
+ // Second pass: handle `mode = url` or `mode = form`
+ for meta in &metas {
+ if let Meta::NameValue(nv) = meta {
+ if let Some(ident) = nv.path.get_ident() {
+ if ident == "mode" {
+ if let Expr::Path(path) = &nv.value {
+ if let Some(k) = path.path.get_ident() {
+ match k.to_string().as_str() {
+ "url" => {
+ let the_url = url_lit.clone().ok_or_else(|| {
+ syn::Error::new_spanned(nv, "when `mode = url`, you must also provide `url = \"https://...\"`")
+ })?;
+ mode = ElicitMode::Url { url: the_url };
+ }
+ "form" => {
+ mode = ElicitMode::Form;
+ }
+ _ => {
+ return Err(syn::Error::new_spanned(
+ k,
+ "mode must be `form` or `url`",
+ ))
+ }
+ }
+ }
+ } else {
+ return Err(syn::Error::new_spanned(
+ &nv.value,
+ "mode must be `form` or `url`",
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ let message = message.unwrap_or_else(|| LitStr::new("", proc_macro2::Span::call_site()));
+
+ Ok(Self { message, mode })
+ }
+}
diff --git a/crates/rust-mcp-macros/src/lib.rs b/crates/rust-mcp-macros/src/lib.rs
index 473792c..30bdc71 100644
--- a/crates/rust-mcp-macros/src/lib.rs
+++ b/crates/rust-mcp-macros/src/lib.rs
@@ -1,312 +1,17 @@
extern crate proc_macro;
+mod elicit;
+mod tool;
mod utils;
+use crate::elicit::generator::{generate_form_schema, generate_from_impl};
+use crate::elicit::parser::{ElicitArgs, ElicitMode};
+use crate::tool::generator::{generate_tool_tokens, ToolTokens};
+use crate::tool::parser::{IconThemeDsl, McpToolMacroAttributes};
use proc_macro::TokenStream;
use quote::quote;
-use syn::{
- parse::Parse, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Error, Expr,
- ExprLit, Fields, GenericArgument, Lit, Meta, PathArguments, Token, Type,
-};
-use utils::{is_option, renamed_field, type_to_json_schema};
-
-/// Represents the attributes for the `mcp_tool` procedural macro.
-///
-/// This struct parses and validates the attributes provided to the `mcp_tool` macro.
-/// The `name` and `description` attributes are required and must not be empty strings.
-///
-/// # Fields
-/// * `name` - A string representing the tool's name (required).
-/// * `description` - A string describing the tool (required).
-/// * `meta` - An optional JSON string for metadata.
-/// * `title` - An optional string for the tool's title.
-/// * The following fields are available only with the `2025_03_26` feature and later:
-/// * `destructive_hint` - Optional boolean for `ToolAnnotations::destructive_hint`.
-/// * `idempotent_hint` - Optional boolean for `ToolAnnotations::idempotent_hint`.
-/// * `open_world_hint` - Optional boolean for `ToolAnnotations::open_world_hint`.
-/// * `read_only_hint` - Optional boolean for `ToolAnnotations::read_only_hint`.
-///
-struct McpToolMacroAttributes {
- name: Option,
- description: Option,
- #[cfg(feature = "2025_06_18")]
- meta: Option, // Store raw JSON string instead of parsed Map
- #[cfg(feature = "2025_06_18")]
- title: Option,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- destructive_hint: Option,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- idempotent_hint: Option,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- open_world_hint: Option,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- read_only_hint: Option,
-}
-
-use syn::parse::ParseStream;
-
-use crate::utils::{generate_enum_parse, is_enum};
-
-struct ExprList {
- exprs: Punctuated,
-}
-
-impl Parse for ExprList {
- fn parse(input: ParseStream) -> syn::Result {
- Ok(ExprList {
- exprs: Punctuated::parse_terminated(input)?,
- })
- }
-}
-
-impl Parse for McpToolMacroAttributes {
- /// Parses the macro attributes from a `ParseStream`.
- ///
- /// This implementation extracts `name`, `description`, `meta`, and `title` from the attribute input.
- /// The `name` and `description` must be provided as string literals and be non-empty.
- /// The `meta` attribute must be a valid JSON object provided as a string literal, and `title` must be a string literal.
- ///
- /// # Errors
- /// Returns a `syn::Error` if:
- /// - The `name` attribute is missing or empty.
- /// - The `description` attribute is missing or empty.
- /// - The `meta` attribute is provided but is not a valid JSON object.
- /// - The `title` attribute is provided but is not a string literal.
- fn parse(attributes: syn::parse::ParseStream) -> syn::Result {
- let mut instance = Self {
- name: None,
- description: None,
- #[cfg(feature = "2025_06_18")]
- meta: None,
- #[cfg(feature = "2025_06_18")]
- title: None,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- destructive_hint: None,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- idempotent_hint: None,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- open_world_hint: None,
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- read_only_hint: 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();
-
- match ident_str.as_str() {
- "name" | "description" => {
- 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!(...)",
- ));
- }
- };
- match ident_str.as_str() {
- "name" => instance.name = Some(value),
- "description" => instance.description = Some(value),
- _ => {}
- }
- }
- #[cfg(feature = "2025_06_18")]
- "meta" => {
- let value = match &meta_name_value.value {
- Expr::Lit(ExprLit {
- lit: Lit::Str(lit_str),
- ..
- }) => lit_str.value(),
- _ => {
- return Err(Error::new_spanned(
- &meta_name_value.value,
- "Expected a JSON object as a string literal",
- ));
- }
- };
- // Validate that the string is a valid JSON object
- let parsed: serde_json::Value =
- serde_json::from_str(&value).map_err(|e| {
- Error::new_spanned(
- &meta_name_value.value,
- format!("Expected a valid JSON object: {e}"),
- )
- })?;
- if !parsed.is_object() {
- return Err(Error::new_spanned(
- &meta_name_value.value,
- "Expected a JSON object",
- ));
- }
- instance.meta = Some(value);
- }
- #[cfg(feature = "2025_06_18")]
- "title" => {
- let value = match &meta_name_value.value {
- Expr::Lit(ExprLit {
- lit: Lit::Str(lit_str),
- ..
- }) => lit_str.value(),
- _ => {
- return Err(Error::new_spanned(
- &meta_name_value.value,
- "Expected a string literal",
- ));
- }
- };
- instance.title = Some(value);
- }
- "destructive_hint" | "idempotent_hint" | "open_world_hint"
- | "read_only_hint" => {
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- {
- let value = match &meta_name_value.value {
- Expr::Lit(ExprLit {
- lit: Lit::Bool(lit_bool),
- ..
- }) => lit_bool.value,
- _ => {
- return Err(Error::new_spanned(
- &meta_name_value.value,
- "Expected a boolean literal",
- ));
- }
- };
-
- match ident_str.as_str() {
- "destructive_hint" => instance.destructive_hint = Some(value),
- "idempotent_hint" => instance.idempotent_hint = Some(value),
- "open_world_hint" => instance.open_world_hint = Some(value),
- "read_only_hint" => instance.read_only_hint = Some(value),
- _ => {}
- }
- }
- }
- _ => {}
- }
- }
- }
-
- // Validate presence and non-emptiness
- if instance
- .name
- .as_ref()
- .map(|s| s.trim().is_empty())
- .unwrap_or(true)
- {
- return Err(Error::new(
- attributes.span(),
- "The 'name' attribute is required and must not be empty.",
- ));
- }
- if instance
- .description
- .as_ref()
- .map(|s| s.trim().is_empty())
- .unwrap_or(true)
- {
- return Err(Error::new(
- attributes.span(),
- "The 'description' attribute is required and must not be empty.",
- ));
- }
-
- Ok(instance)
- }
-}
-
-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)
- }
-}
+use syn::{parse_macro_input, Data, DeriveInput, Fields};
+use utils::{base_crate, is_option, is_vec_string, renamed_field, type_to_json_schema};
/// A procedural macro attribute to generate rust_mcp_schema::Tool related utility methods for a struct.
///
@@ -357,84 +62,22 @@ impl Parse for McpElicitationAttributes {
pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let input_ident = &input.ident;
-
- // Conditionally select the path for Tool
- 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 McpToolMacroAttributes);
- let tool_name = macro_attributes.name.unwrap_or_default();
- let tool_description = macro_attributes.description.unwrap_or_default();
-
- #[cfg(not(feature = "2025_06_18"))]
- let meta = quote! {};
- #[cfg(feature = "2025_06_18")]
- let meta = macro_attributes.meta.map_or(quote! { meta: None, }, |m| {
- quote! { meta: Some(serde_json::from_str(#m).expect("Failed to parse meta JSON")), }
- });
-
- #[cfg(not(feature = "2025_06_18"))]
- let title = quote! {};
- #[cfg(feature = "2025_06_18")]
- let title = macro_attributes.title.map_or(
- quote! { title: None, },
- |t| quote! { title: Some(#t.to_string()), },
- );
-
- #[cfg(not(feature = "2025_06_18"))]
- let output_schema = quote! {};
- #[cfg(feature = "2025_06_18")]
- let output_schema = quote! { output_schema: None,};
-
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- let some_annotations = macro_attributes.destructive_hint.is_some()
- || macro_attributes.idempotent_hint.is_some()
- || macro_attributes.open_world_hint.is_some()
- || macro_attributes.read_only_hint.is_some();
-
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- let annotations = if some_annotations {
- let destructive_hint = macro_attributes
- .destructive_hint
- .map_or(quote! {None}, |v| quote! {Some(#v)});
-
- let idempotent_hint = macro_attributes
- .idempotent_hint
- .map_or(quote! {None}, |v| quote! {Some(#v)});
- let open_world_hint = macro_attributes
- .open_world_hint
- .map_or(quote! {None}, |v| quote! {Some(#v)});
- let read_only_hint = macro_attributes
- .read_only_hint
- .map_or(quote! {None}, |v| quote! {Some(#v)});
- quote! {
- Some(#base_crate::ToolAnnotations {
- destructive_hint: #destructive_hint,
- idempotent_hint: #idempotent_hint,
- open_world_hint: #open_world_hint,
- read_only_hint: #read_only_hint,
- title: None,
- })
- }
- } else {
- quote! { None }
- };
-
- let annotations_token = {
- #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
- {
- quote! { annotations: #annotations, }
- }
- #[cfg(not(any(feature = "2025_03_26", feature = "2025_06_18")))]
- {
- quote! {}
- }
- };
-
+ let ToolTokens {
+ base_crate,
+ tool_name,
+ tool_description,
+ meta,
+ title,
+ output_schema,
+ annotations,
+ execution,
+ icons,
+ } = generate_tool_tokens(macro_attributes);
+
+ // TODO: add support for schema version to ToolInputSchema :
+ // it defaults to JSON Schema 2020-12 when no explicit $schema is provided.
let tool_token = quote! {
#base_crate::Tool {
name: #tool_name.to_string(),
@@ -442,8 +85,10 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
#output_schema
#title
#meta
- #annotations_token
- input_schema: #base_crate::ToolInputSchema::new(required, properties)
+ #annotations
+ #execution
+ #icons
+ input_schema: #base_crate::ToolInputSchema::new(required, properties, None)
}
};
@@ -454,6 +99,27 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
#tool_name.to_string()
}
+ /// Returns a `CallToolRequestParams` initialized with the current tool's name.
+ ///
+ /// You can further customize the request by adding arguments or other attributes
+ /// using the builder pattern. For example:
+ ///
+ /// ```ignore
+ /// # use my_crate::{MyTool};
+ /// let args = serde_json::Map::new();
+ /// let task_meta = TaskMetadata{ttl: Some(200)}
+ ///
+ /// let params: CallToolRequestParams = MyTool::request_params()
+ /// .with_arguments(args)
+ /// .with_task(task_meta);
+ /// ```
+ ///
+ /// # Returns
+ /// A `CallToolRequestParams` with the tool name set.
+ pub fn request_params() -> #base_crate::CallToolRequestParams {
+ #base_crate::CallToolRequestParams::new(#tool_name.to_string())
+ }
+
/// Constructs and returns a `rust_mcp_schema::Tool` instance.
///
/// The tool includes the name, description, input schema, meta, and title derived from
@@ -503,300 +169,118 @@ pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
}
#[proc_macro_attribute]
-pub fn mcp_elicit(attributes: TokenStream, input: TokenStream) -> TokenStream {
+pub fn mcp_elicit(args: 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 fields = match &input.data {
+ Data::Struct(s) => match &s.fields {
+ Fields::Named(n) => &n.named,
+ _ => panic!("mcp_elicit only supports structs with named fields"),
+ },
+ _ => panic!("mcp_elicit only supports structs"),
};
- let macro_attributes = parse_macro_input!(attributes as McpElicitationAttributes);
- let message = macro_attributes.message.unwrap_or_default();
+ let struct_name = &input.ident;
+ let elicit_args = parse_macro_input!(args as ElicitArgs);
- // 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 base_crate = base_crate();
- 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
- )));
- }
- };
- }
- };
+ let message = &elicit_args.message;
- type_check
- });
+ let impl_block = match elicit_args.mode {
+ ElicitMode::Form => {
+ let (from_content, init) = generate_from_impl(fields, &base_crate);
+ let schema = generate_form_schema(struct_name, &base_crate);
- let field_idents = fields.named.iter().map(|field| &field.ident);
+ quote! {
+ impl #struct_name {
+ pub fn message() -> &'static str{
+ #message
+ }
- quote! {
- #(#assignments)*
+ pub fn requested_schema() -> #base_crate::ElicitFormSchema {
+ #schema
+ }
- Ok(Self {
- #(#field_idents,)*
- })
- }
- }
- _ => panic!("mcp_elicit macro only supports structs with named fields"),
- },
- _ => panic!("mcp_elicit macro only supports structs"),
- };
+ pub fn elicit_mode()->&'static str{
+ "form"
+ }
- let output = quote! {
- impl #input_ident {
+ pub fn elicit_form_params() -> #base_crate::ElicitRequestFormParams {
+ #base_crate::ElicitRequestFormParams::new(
+ Self::message().to_string(),
+ Self::requested_schema(),
+ None,
+ None,
+ )
+ }
- /// 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()
- }
+ pub fn elicit_request_params() -> #base_crate::ElicitRequestParams {
+ Self::elicit_form_params().into()
+ }
- /// 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();
+ pub fn from_elicit_result_content(
+ mut content: Option>,
+ ) -> Result {
+ use #base_crate::{ElicitResultContent as V, RpcError};
+ let mut map = content.take().unwrap_or_default();
+ #from_content
+ Ok(#init)
+ }
- 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(),
- };
+ }
+ }
+ }
+ ElicitMode::Url { url } => {
+ let (from_content, init) = generate_from_impl(fields, &base_crate);
- 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()
- });
+ quote! {
+ impl #struct_name {
+ pub fn message() -> &'static str {
+ #message
+ }
- 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();
+ pub fn url() -> &'static str {
+ #url
+ }
- let properties =
- properties.expect("Was not able to create a ElicitRequestedSchema");
+ pub fn elicit_mode()->&'static str {
+ "url"
+ }
- let requested_schema = #base_crate::ElicitRequestedSchema::new(properties, required);
- requested_schema
- }
+ pub fn elicit_url_params(elicitation_id:String) -> #base_crate::ElicitRequestUrlParams {
+ #base_crate::ElicitRequestUrlParams::new(
+ elicitation_id,
+ Self::message().to_string(),
+ Self::url().to_string(),
+ None,
+ None,
+ )
+ }
- /// 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
+ pub fn elicit_request_params(elicitation_id:String) -> #base_crate::ElicitRequestParams {
+ Self::elicit_url_params(elicitation_id).into()
+ }
+
+ pub fn from_elicit_result_content(
+ mut content: Option>,
+ ) -> Result {
+ use #base_crate::{ElicitResultContent as V, RpcError};
+ let mut map = content.take().unwrap_or_default();
+ #from_content
+ Ok(#init)
+ }
+ }
}
}
+ };
+
+ let expanded = quote! {
#input
+ #impl_block
};
- TokenStream::from(output)
+ TokenStream::from(expanded)
}
/// Derives a JSON Schema representation for a struct.
@@ -1051,84 +535,3 @@ pub fn derive_json_schema(input: TokenStream) -> TokenStream {
};
TokenStream::from(expanded)
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use syn::parse_str;
- #[test]
- fn test_valid_macro_attributes() {
- let input = r#"name = "test_tool", description = "A test tool.", meta = "{\"version\": \"1.0\"}", title = "Test Tool""#;
- let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
-
- assert_eq!(parsed.name.unwrap(), "test_tool");
- assert_eq!(parsed.description.unwrap(), "A test tool.");
- assert_eq!(parsed.meta.unwrap(), "{\"version\": \"1.0\"}");
- assert_eq!(parsed.title.unwrap(), "Test Tool");
- }
-
- #[test]
- fn test_missing_name() {
- let input = r#"description = "Only description""#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert_eq!(
- result.err().unwrap().to_string(),
- "The 'name' attribute is required and must not be empty."
- );
- }
-
- #[test]
- fn test_missing_description() {
- let input = r#"name = "OnlyName""#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert_eq!(
- result.err().unwrap().to_string(),
- "The 'description' attribute is required and must not be empty."
- );
- }
-
- #[test]
- fn test_empty_name_field() {
- let input = r#"name = "", description = "something""#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert_eq!(
- result.err().unwrap().to_string(),
- "The 'name' attribute is required and must not be empty."
- );
- }
-
- #[test]
- fn test_empty_description_field() {
- let input = r#"name = "my-tool", description = """#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert_eq!(
- result.err().unwrap().to_string(),
- "The 'description' attribute is required and must not be empty."
- );
- }
-
- #[test]
- fn test_invalid_meta() {
- let input =
- r#"name = "test_tool", description = "A test tool.", meta = "not_a_json_object""#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert!(result
- .err()
- .unwrap()
- .to_string()
- .contains("Expected a valid JSON object"));
- }
-
- #[test]
- fn test_non_object_meta() {
- let input = r#"name = "test_tool", description = "A test tool.", meta = "[1, 2, 3]""#;
- let result: Result = parse_str(input);
- assert!(result.is_err());
- assert_eq!(result.err().unwrap().to_string(), "Expected a JSON object");
- }
-}
diff --git a/crates/rust-mcp-macros/src/tool.rs b/crates/rust-mcp-macros/src/tool.rs
new file mode 100644
index 0000000..1ffdea9
--- /dev/null
+++ b/crates/rust-mcp-macros/src/tool.rs
@@ -0,0 +1,2 @@
+pub(crate) mod generator;
+pub(crate) mod parser;
diff --git a/crates/rust-mcp-macros/src/tool/generator.rs b/crates/rust-mcp-macros/src/tool/generator.rs
new file mode 100644
index 0000000..a7c1e82
--- /dev/null
+++ b/crates/rust-mcp-macros/src/tool/generator.rs
@@ -0,0 +1,184 @@
+use crate::tool::parser::ExecutionSupportDsl;
+use crate::utils::base_crate;
+use crate::IconThemeDsl;
+use crate::McpToolMacroAttributes;
+use proc_macro2::TokenStream;
+use quote::quote;
+
+pub struct ToolTokens {
+ pub base_crate: TokenStream,
+ pub tool_name: String,
+ pub tool_description: String,
+ pub meta: TokenStream,
+ pub title: TokenStream,
+ pub output_schema: TokenStream,
+ pub annotations: TokenStream,
+ pub execution: TokenStream,
+ pub icons: TokenStream,
+}
+
+pub fn generate_tool_tokens(macro_attributes: McpToolMacroAttributes) -> ToolTokens {
+ // Conditionally select the path for Tool
+ let base_crate = base_crate();
+ let tool_name = macro_attributes.name.clone().unwrap_or_default();
+ let tool_description = macro_attributes.description.clone().unwrap_or_default();
+
+ let title = macro_attributes.title.as_ref().map_or(
+ quote! { title: None, },
+ |t| quote! { title: Some(#t.to_string()), },
+ );
+
+ let meta = macro_attributes
+ .meta
+ .as_ref()
+ .map_or(quote! { meta: None, }, |m| {
+ quote! { meta: Some(serde_json::from_str(#m).expect("Failed to parse meta JSON")), }
+ });
+
+ //TODO: add support for output_schema
+ let output_schema = quote! { output_schema: None,};
+
+ let annotations = generate_annotations(&base_crate, ¯o_attributes);
+ let execution = generate_executions(&base_crate, ¯o_attributes);
+ let icons = generate_icons(&base_crate, ¯o_attributes);
+
+ ToolTokens {
+ base_crate,
+ tool_name,
+ tool_description,
+ meta,
+ title,
+ output_schema,
+ annotations,
+ execution,
+ icons,
+ }
+}
+
+fn generate_icons(
+ base_crate: &TokenStream,
+ macro_attributes: &McpToolMacroAttributes,
+) -> TokenStream {
+ let mut icon_exprs = Vec::new();
+
+ if let Some(icons) = ¯o_attributes.icons {
+ for icon in icons {
+ let src = &icon.src;
+ let mime_type = icon
+ .mime_type
+ .as_ref()
+ .map(|s| quote! { Some(#s.to_string()) })
+ .unwrap_or(quote! { None });
+ let theme = icon
+ .theme
+ .as_ref()
+ .map(|t| match t {
+ IconThemeDsl::Light => quote! { Some(#base_crate::IconTheme::Light) },
+ IconThemeDsl::Dark => quote! { Some(#base_crate::IconTheme::Dark) },
+ })
+ .unwrap_or(quote! { None });
+
+ // Build sizes: Vec
+ let sizes: Vec<_> = icon
+ .sizes
+ .as_ref()
+ .map(|arr| {
+ arr.elems
+ .iter()
+ .map(|elem| {
+ if let syn::Expr::Lit(expr_lit) = elem {
+ if let syn::Lit::Str(lit_str) = &expr_lit.lit {
+ let val = lit_str.value();
+ return quote! { #val.to_string() };
+ }
+ }
+ panic!("sizes must contain only string literals");
+ })
+ .collect::>()
+ })
+ .unwrap_or_default();
+
+ let icon_expr = quote! {
+ #base_crate::Icon {
+ src: #src.to_string(),
+ mime_type: #mime_type,
+ sizes: vec![ #(#sizes),* ],
+ theme: #theme,
+ }
+ };
+ icon_exprs.push(icon_expr);
+ }
+ }
+
+ if icon_exprs.is_empty() {
+ quote! { icons: ::std::vec::Vec::new(), }
+ } else {
+ quote! { icons: vec![ #(#icon_exprs),* ], }
+ }
+}
+
+fn generate_executions(
+ base_crate: &TokenStream,
+ macro_attributes: &McpToolMacroAttributes,
+) -> TokenStream {
+ if let Some(exec) = macro_attributes.execution.as_ref() {
+ let task_support = match exec {
+ ExecutionSupportDsl::Forbidden => {
+ quote! { Some(#base_crate::ToolExecutionTaskSupport::Forbidden) }
+ }
+ ExecutionSupportDsl::Optional => {
+ quote! { Some(#base_crate::ToolExecutionTaskSupport::Optional) }
+ }
+ ExecutionSupportDsl::Required => {
+ quote! { Some(#base_crate::ToolExecutionTaskSupport::Required) }
+ }
+ };
+
+ quote! {
+ execution: Some(#base_crate::ToolExecution {
+ task_support: #task_support,
+ }),
+ }
+ } else {
+ quote! { execution: None, }
+ }
+}
+
+fn generate_annotations(
+ base_crate: &TokenStream,
+ macro_attributes: &McpToolMacroAttributes,
+) -> TokenStream {
+ let some_annotations = macro_attributes.destructive_hint.is_some()
+ || macro_attributes.idempotent_hint.is_some()
+ || macro_attributes.open_world_hint.is_some()
+ || macro_attributes.read_only_hint.is_some();
+
+ let annotations = if some_annotations {
+ let destructive_hint = macro_attributes
+ .destructive_hint
+ .map_or(quote! {None}, |v| quote! {Some(#v)});
+
+ let idempotent_hint = macro_attributes
+ .idempotent_hint
+ .map_or(quote! {None}, |v| quote! {Some(#v)});
+ let open_world_hint = macro_attributes
+ .open_world_hint
+ .map_or(quote! {None}, |v| quote! {Some(#v)});
+ let read_only_hint = macro_attributes
+ .read_only_hint
+ .map_or(quote! {None}, |v| quote! {Some(#v)});
+ quote! {
+ Some(#base_crate::ToolAnnotations {
+ destructive_hint: #destructive_hint,
+ idempotent_hint: #idempotent_hint,
+ open_world_hint: #open_world_hint,
+ read_only_hint: #read_only_hint,
+ title: None,
+ })
+ }
+ } else {
+ quote! { None }
+ };
+
+ quote! { annotations: #annotations, }
+}
diff --git a/crates/rust-mcp-macros/src/tool/parser.rs b/crates/rust-mcp-macros/src/tool/parser.rs
new file mode 100644
index 0000000..fbcfee2
--- /dev/null
+++ b/crates/rust-mcp-macros/src/tool/parser.rs
@@ -0,0 +1,509 @@
+use quote::ToTokens;
+use syn::parenthesized;
+use syn::parse::ParseStream;
+use syn::spanned::Spanned;
+use syn::ExprArray;
+use syn::{
+ parse::Parse, punctuated::Punctuated, Error, Expr, ExprLit, Ident, Lit, LitStr, Meta, Token,
+};
+
+struct ExprList {
+ exprs: Punctuated,
+}
+
+impl Parse for ExprList {
+ fn parse(input: ParseStream) -> syn::Result {
+ Ok(ExprList {
+ exprs: Punctuated::parse_terminated(input)?,
+ })
+ }
+}
+
+/// Represents the attributes for the `mcp_tool` procedural macro.
+///
+/// This struct parses and validates the attributes provided to the `mcp_tool` macro.
+/// The `name` and `description` attributes are required and must not be empty strings.
+///
+/// # Fields
+/// * `name` - A string representing the tool's name (required).
+/// * `description` - A string describing the tool (required).
+/// * `meta` - An optional JSON string for metadata.
+/// * `title` - An optional string for the tool's title.
+/// * The following fields are available only with the `2025_03_26` feature and later:
+/// * `destructive_hint` - Optional boolean for `ToolAnnotations::destructive_hint`.
+/// * `idempotent_hint` - Optional boolean for `ToolAnnotations::idempotent_hint`.
+/// * `open_world_hint` - Optional boolean for `ToolAnnotations::open_world_hint`.
+/// * `read_only_hint` - Optional boolean for `ToolAnnotations::read_only_hint`.
+///
+pub(crate) struct McpToolMacroAttributes {
+ pub name: Option,
+ pub description: Option,
+ pub meta: Option, // Store raw JSON string instead of parsed Map
+ pub title: Option,
+ pub destructive_hint: Option,
+ pub idempotent_hint: Option,
+ pub open_world_hint: Option,
+ pub read_only_hint: Option,
+ pub execution: Option,
+ pub icons: Option>,
+}
+
+pub(crate) enum ExecutionSupportDsl {
+ Forbidden,
+ Optional,
+ Required,
+}
+
+pub(crate) struct IconDsl {
+ pub(crate) src: LitStr,
+ pub(crate) mime_type: Option,
+ pub(crate) sizes: Option,
+ pub(crate) theme: Option,
+}
+
+pub(crate) enum IconThemeDsl {
+ Light,
+ Dark,
+}
+
+pub(crate) struct IconField {
+ pub(crate) key: Ident,
+ pub(crate) _eq_token: Token![=],
+ pub(crate) value: syn::Expr,
+}
+
+impl Parse for IconField {
+ fn parse(input: ParseStream) -> syn::Result {
+ Ok(IconField {
+ key: input.parse()?,
+ _eq_token: input.parse()?,
+ value: input.parse()?,
+ })
+ }
+}
+
+impl Parse for IconDsl {
+ fn parse(input: ParseStream) -> syn::Result {
+ let content;
+ parenthesized!(content in input); // parse ( ... )
+
+ let fields: Punctuated =
+ content.parse_terminated(IconField::parse, Token![,])?;
+
+ let mut src = None;
+ let mut mime_type = None;
+ let mut sizes = None;
+ let mut theme = None;
+
+ for field in fields {
+ let key_str = field.key.to_string();
+ match key_str.as_str() {
+ "src" => {
+ if let syn::Expr::Lit(expr_lit) = field.value {
+ if let syn::Lit::Str(lit) = expr_lit.lit {
+ src = Some(lit);
+ } else {
+ return Err(syn::Error::new(
+ expr_lit.span(),
+ "expected string literal for src",
+ ));
+ }
+ }
+ }
+ "mime_type" => {
+ if let syn::Expr::Lit(expr_lit) = field.value {
+ if let syn::Lit::Str(lit) = expr_lit.lit {
+ mime_type = Some(lit);
+ } else {
+ return Err(syn::Error::new(
+ expr_lit.span(),
+ "expected string literal for mime_type",
+ ));
+ }
+ }
+ }
+ "sizes" => {
+ if let syn::Expr::Array(arr) = field.value {
+ // Validate that every element is a string literal.
+ for elem in &arr.elems {
+ match elem {
+ syn::Expr::Lit(expr_lit) => {
+ if let syn::Lit::Str(_) = &expr_lit.lit {
+ // ok
+ } else {
+ return Err(syn::Error::new(
+ expr_lit.span(),
+ "sizes array must contain string literals",
+ ));
+ }
+ }
+ _ => {
+ return Err(syn::Error::new(
+ elem.span(),
+ "sizes array must contain only string literals",
+ ));
+ }
+ }
+ }
+
+ sizes = Some(arr);
+ } else {
+ return Err(syn::Error::new(
+ field.value.span(),
+ "expected array expression for sizes",
+ ));
+ }
+ }
+ "theme" => {
+ if let syn::Expr::Lit(expr_lit) = field.value {
+ if let syn::Lit::Str(lit) = expr_lit.lit {
+ theme = Some(match lit.value().as_str() {
+ "light" => IconThemeDsl::Light,
+ "dark" => IconThemeDsl::Dark,
+ _ => {
+ return Err(syn::Error::new(
+ lit.span(),
+ "theme must be \"light\" or \"dark\"",
+ ));
+ }
+ });
+ }
+ }
+ }
+ _ => {
+ return Err(syn::Error::new(
+ field.key.span(),
+ "unexpected field in icon",
+ ))
+ }
+ }
+ }
+
+ Ok(IconDsl {
+ src: src.ok_or_else(|| syn::Error::new(input.span(), "icon must have `src`"))?,
+ mime_type,
+ sizes,
+ theme,
+ })
+ }
+}
+
+impl Parse for IconThemeDsl {
+ fn parse(_input: ParseStream) -> syn::Result {
+ panic!("IconThemeDsl should be parsed inside IconDsl")
+ }
+}
+
+impl Parse for McpToolMacroAttributes {
+ /// Parses the macro attributes from a `ParseStream`.
+ ///
+ /// This implementation extracts `name`, `description`, `meta`, and `title` from the attribute input.
+ /// The `name` and `description` must be provided as string literals and be non-empty.
+ /// The `meta` attribute must be a valid JSON object provided as a string literal, and `title` must be a string literal.
+ ///
+ /// # Errors
+ /// Returns a `syn::Error` if:
+ /// - The `name` attribute is missing or empty.
+ /// - The `description` attribute is missing or empty.
+ /// - The `meta` attribute is provided but is not a valid JSON object.
+ /// - The `title` attribute is provided but is not a string literal.
+ fn parse(attributes: syn::parse::ParseStream) -> syn::Result {
+ let mut instance = Self {
+ name: None,
+ description: None,
+ meta: None,
+ title: None,
+ destructive_hint: None,
+ idempotent_hint: None,
+ open_world_hint: None,
+ read_only_hint: None,
+ execution: None,
+ icons: None,
+ };
+
+ let meta_list: Punctuated = Punctuated::parse_terminated(attributes)?;
+ for meta in meta_list {
+ match meta {
+ Meta::NameValue(meta_name_value) => {
+ let ident = meta_name_value.path.get_ident().unwrap();
+ let ident_str = ident.to_string();
+
+ match ident_str.as_str() {
+ "name" | "description" => {
+ 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!(...)",
+ ));
+ }
+ };
+ match ident_str.as_str() {
+ "name" => instance.name = Some(value),
+ "description" => instance.description = Some(value),
+ _ => {}
+ }
+ }
+ "meta" => {
+ let value = match &meta_name_value.value {
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) => lit_str.value(),
+ _ => {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected a JSON object as a string literal",
+ ));
+ }
+ };
+ // Validate that the string is a valid JSON object
+ let parsed: serde_json::Value =
+ serde_json::from_str(&value).map_err(|e| {
+ Error::new_spanned(
+ &meta_name_value.value,
+ format!("Expected a valid JSON object: {e}"),
+ )
+ })?;
+ if !parsed.is_object() {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected a JSON object",
+ ));
+ }
+ instance.meta = Some(value);
+ }
+ "title" => {
+ let value = match &meta_name_value.value {
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) => lit_str.value(),
+ _ => {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected a string literal",
+ ));
+ }
+ };
+ instance.title = Some(value);
+ }
+ "destructive_hint" | "idempotent_hint" | "open_world_hint"
+ | "read_only_hint" => {
+ let value = match &meta_name_value.value {
+ Expr::Lit(ExprLit {
+ lit: Lit::Bool(lit_bool),
+ ..
+ }) => lit_bool.value,
+ _ => {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected a boolean literal",
+ ));
+ }
+ };
+
+ match ident_str.as_str() {
+ "destructive_hint" => instance.destructive_hint = Some(value),
+ "idempotent_hint" => instance.idempotent_hint = Some(value),
+ "open_world_hint" => instance.open_world_hint = Some(value),
+ "read_only_hint" => instance.read_only_hint = Some(value),
+ _ => {}
+ }
+ }
+ "icons" => {
+ // Check if the value is an array (Expr::Array)
+ if let Expr::Array(array_expr) = &meta_name_value.value {
+ let icon_list: Punctuated = array_expr
+ .elems
+ .iter()
+ .map(|elem| syn::parse2::(elem.to_token_stream()))
+ .collect::>()?;
+ instance.icons = Some(icon_list.into_iter().collect());
+ } else {
+ return Err(Error::new_spanned(
+ &meta_name_value.value,
+ "Expected an array for the 'icons' attribute",
+ ));
+ }
+ }
+ other => {
+ eprintln!("other: {:?}", other)
+ }
+ }
+ }
+ Meta::List(meta_list) => {
+ let ident = meta_list.path.get_ident().unwrap();
+ let ident_str = ident.to_string();
+
+ if ident_str == "execution" {
+ let nested = meta_list
+ .parse_args_with(Punctuated::::parse_terminated)?;
+ let mut task_support = None;
+
+ for meta in nested {
+ if let Meta::NameValue(nv) = meta {
+ if nv.path.is_ident("task_support") {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(s), ..
+ }) = &nv.value
+ {
+ let value = s.value();
+ task_support = Some(match value.as_str() {
+ "forbidden" => ExecutionSupportDsl::Forbidden,
+ "optional" => ExecutionSupportDsl::Optional,
+ "required" => ExecutionSupportDsl::Required,
+ _ => return Err(Error::new_spanned(&nv.value, "task_support must be one of: forbidden, optional, required")),
+ });
+ }
+ }
+ }
+ }
+
+ instance.execution = task_support;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ // Validate presence and non-emptiness
+ if instance
+ .name
+ .as_ref()
+ .map(|s| s.trim().is_empty())
+ .unwrap_or(true)
+ {
+ return Err(Error::new(
+ attributes.span(),
+ "The 'name' attribute is required and must not be empty.",
+ ));
+ }
+
+ if instance
+ .description
+ .as_ref()
+ .map(|s| s.trim().is_empty())
+ .unwrap_or(true)
+ {
+ return Err(Error::new(
+ attributes.span(),
+ "The 'description' attribute is required and must not be empty.",
+ ));
+ }
+
+ Ok(instance)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use syn::parse_str;
+ #[test]
+ fn test_valid_macro_attributes() {
+ let input = r#"name = "test_tool", description = "A test tool.", meta = "{\"version\": \"1.0\"}", title = "Test Tool""#;
+ let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
+
+ assert_eq!(parsed.name.unwrap(), "test_tool");
+ assert_eq!(parsed.description.unwrap(), "A test tool.");
+ assert_eq!(parsed.meta.unwrap(), "{\"version\": \"1.0\"}");
+ assert_eq!(parsed.title.unwrap(), "Test Tool");
+ }
+
+ #[test]
+ fn test_missing_name() {
+ let input = r#"description = "Only description""#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "The 'name' attribute is required and must not be empty."
+ );
+ }
+
+ #[test]
+ fn test_missing_description() {
+ let input = r#"name = "OnlyName""#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "The 'description' attribute is required and must not be empty."
+ );
+ }
+
+ #[test]
+ fn test_empty_name_field() {
+ let input = r#"name = "", description = "something""#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "The 'name' attribute is required and must not be empty."
+ );
+ }
+
+ #[test]
+ fn test_empty_description_field() {
+ let input = r#"name = "my-tool", description = """#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "The 'description' attribute is required and must not be empty."
+ );
+ }
+
+ #[test]
+ fn test_invalid_meta() {
+ let input =
+ r#"name = "test_tool", description = "A test tool.", meta = "not_a_json_object""#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert!(result
+ .err()
+ .unwrap()
+ .to_string()
+ .contains("Expected a valid JSON object"));
+ }
+
+ #[test]
+ fn test_non_object_meta() {
+ let input = r#"name = "test_tool", description = "A test tool.", meta = "[1, 2, 3]""#;
+ let result: Result = parse_str(input);
+ assert!(result.is_err());
+ assert_eq!(result.err().unwrap().to_string(), "Expected a JSON object");
+ }
+}
diff --git a/crates/rust-mcp-macros/src/utils.rs b/crates/rust-mcp-macros/src/utils.rs
index 71d3de3..ca1c4c2 100644
--- a/crates/rust-mcp-macros/src/utils.rs
+++ b/crates/rust-mcp-macros/src/utils.rs
@@ -1,9 +1,19 @@
+use proc_macro2::TokenStream;
use quote::quote;
use syn::{
- punctuated::Punctuated, token, Attribute, DeriveInput, Lit, LitInt, LitStr, Path,
- PathArguments, Type,
+ punctuated::Punctuated, token, Attribute, DeriveInput, GenericArgument, Lit, LitInt, LitStr,
+ Path, PathArguments, Type, TypePath,
};
+pub fn base_crate() -> TokenStream {
+ // Conditionally select the path for Tool
+ if cfg!(feature = "sdk") {
+ quote! { rust_mcp_sdk::schema }
+ } else {
+ quote! { rust_mcp_schema }
+ }
+}
+
// Check if a type is an Option
pub fn is_option(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
@@ -84,6 +94,7 @@ pub fn might_be_struct(ty: &Type) -> bool {
false
}
+#[allow(unused)]
// 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 {
@@ -108,6 +119,7 @@ pub fn is_enum(ty: &Type, _input: &DeriveInput) -> bool {
}
}
+#[allow(unused)]
// Helper to generate enum parsing code
pub fn generate_enum_parse(
field_type: &Type,
@@ -457,6 +469,38 @@ pub fn has_derive(attrs: &[Attribute], trait_name: &str) -> bool {
})
}
+pub fn is_vec_string(ty: &Type) -> bool {
+ let Type::Path(TypePath { path, .. }) = ty else {
+ return false;
+ };
+
+ // Get last segment: e.g., `Vec`
+ let Some(seg) = path.segments.last() else {
+ return false;
+ };
+
+ // Must be `Vec`
+ if seg.ident != "Vec" {
+ return false;
+ }
+
+ // Must have angle-bracketed args:
+ let PathArguments::AngleBracketed(args) = &seg.arguments else {
+ return false;
+ };
+
+ // Must contain exactly one type param
+ if args.args.len() != 1 {
+ return false;
+ }
+
+ // Check that the argument is `String`
+ match args.args.first().unwrap() {
+ GenericArgument::Type(Type::Path(tp)) => tp.path.is_ident("String"),
+ _ => false,
+ }
+}
+
pub fn renamed_field(attrs: &[Attribute]) -> Option {
let mut renamed = None;
diff --git a/crates/rust-mcp-macros/tests/common/common.rs b/crates/rust-mcp-macros/tests/common/common.rs
index 1133d64..e754cdb 100644
--- a/crates/rust-mcp-macros/tests/common/common.rs
+++ b/crates/rust-mcp-macros/tests/common/common.rs
@@ -48,30 +48,3 @@ impl FromStr for Colors {
}
}
}
-
-#[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
deleted file mode 100644
index 4b6c926..0000000
--- a/crates/rust-mcp-macros/tests/macro_test.rs
+++ /dev/null
@@ -1,274 +0,0 @@
-#[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;
-
-#[test]
-fn test_rename() {
- let schema = EditOperation::json_schema();
-
- assert_eq!(schema.len(), 3);
-
- assert!(schema.contains_key("properties"));
- assert!(schema.contains_key("required"));
-
- assert!(schema.contains_key("type"));
- assert_eq!(schema.get("type").unwrap(), "object");
-
- let required: Vec<_> = schema
- .get("required")
- .unwrap()
- .as_array()
- .unwrap()
- .iter()
- .filter_map(|v| v.as_str())
- .collect();
-
- assert_eq!(required.len(), 2);
- assert!(required.contains(&"oldText"));
- assert!(required.contains(&"newText"));
-
- 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-macros/tests/test_mcp_elicit.rs b/crates/rust-mcp-macros/tests/test_mcp_elicit.rs
new file mode 100644
index 0000000..dba92d9
--- /dev/null
+++ b/crates/rust-mcp-macros/tests/test_mcp_elicit.rs
@@ -0,0 +1,403 @@
+use rust_mcp_macros::{mcp_elicit, JsonSchema};
+use rust_mcp_schema::{
+ ElicitRequestFormParams, ElicitRequestParams, ElicitRequestUrlParams, ElicitResultContent,
+ RpcError,
+};
+use std::collections::HashMap;
+
+#[test]
+fn test_form_basic_conversion() {
+ // Form elicit basic
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Please enter your name and age", mode=form)]
+ pub struct BasicUser {
+ pub name: String,
+ pub age: Option,
+ pub expertise: Vec,
+ }
+ assert_eq!(BasicUser::message(), "Please enter your name and age");
+ let mut content: std::collections::HashMap = HashMap::new();
+ content.insert(
+ "name".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "Ali".to_string(),
+ )),
+ );
+ content.insert(
+ "age".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Integer(21)),
+ );
+ content.insert(
+ "expertise".to_string(),
+ ElicitResultContent::StringArray(vec!["Rust".to_string(), "C++".to_string()]),
+ );
+
+ let user: BasicUser = BasicUser::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Ali");
+ assert_eq!(user.age, Some(21));
+ assert_eq!(user.expertise, vec!["Rust".to_string(), "C++".to_string()]);
+
+ let req = BasicUser::elicit_request_params();
+ match req {
+ ElicitRequestParams::FormParams(form) => {
+ assert_eq!(form.message, "Please enter your name and age");
+ assert!(form.requested_schema.properties.contains_key("name"));
+ assert!(form.requested_schema.properties.contains_key("age"));
+ assert_eq!(form.requested_schema.required, vec!["name", "expertise"]); // age is optional
+ assert!(form.meta.is_none());
+ assert_eq!(form.mode().as_ref().unwrap(), "form");
+ }
+ _ => panic!("Expected FormParams"),
+ }
+}
+
+#[test]
+fn test_url_basic_conversion() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Please enter your name and age", mode=url, url="https://github.com/rust-mcp-stack/rust-mcp-sdk")]
+ pub struct InfoFromUrl {
+ pub name: String,
+ pub age: Option,
+ pub expertise: Vec,
+ }
+
+ assert_eq!(InfoFromUrl::message(), "Please enter your name and age");
+ assert_eq!(
+ InfoFromUrl::url(),
+ "https://github.com/rust-mcp-stack/rust-mcp-sdk"
+ );
+
+ let mut content: std::collections::HashMap = HashMap::new();
+ content.insert(
+ "name".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "Ali".to_string(),
+ )),
+ );
+ content.insert(
+ "age".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Integer(21)),
+ );
+ content.insert(
+ "expertise".to_string(),
+ ElicitResultContent::StringArray(vec!["Rust".to_string(), "C++".to_string()]),
+ );
+
+ let user: InfoFromUrl = InfoFromUrl::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Ali");
+ assert_eq!(user.age, Some(21));
+ assert_eq!(user.expertise, vec!["Rust".to_string(), "C++".to_string()]);
+ let req = InfoFromUrl::elicit_request_params("elicit_id".to_string());
+ match req {
+ ElicitRequestParams::UrlParams(params) => {
+ assert_eq!(params.message, "Please enter your name and age");
+ assert!(params.meta.is_none());
+ assert!(params.task.is_none());
+ assert_eq!(params.mode(), "url");
+ }
+ _ => panic!("Expected UrlParams"),
+ }
+}
+
+#[test]
+fn test_missing_required_field_returns_error() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Enter user info", mode = form)]
+ pub struct RequiredFields {
+ pub name: String,
+ pub email: String,
+ pub tags: Vec,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "name".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "Alice".to_string(),
+ )),
+ );
+ // Missing 'email' and 'tags' - both required
+
+ let result = RequiredFields::from_elicit_result_content(Some(content));
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_extra_unknown_field_is_ignored() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Test", mode = form)]
+ pub struct StrictStruct {
+ pub name: String,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "name".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "Bob".to_string(),
+ )),
+ );
+ content.insert(
+ "unknown_field".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "ignored".to_string(),
+ )),
+ );
+
+ let user = StrictStruct::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Bob");
+ // unknown_field is silently ignored - correct behavior
+}
+
+#[test]
+fn test_type_mismatch_returns_error() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Bad type", mode = form)]
+ pub struct TypeSensitive {
+ pub age: i32,
+ pub active: bool,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "age".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "not_a_number".to_string(),
+ )),
+ );
+ content.insert(
+ "active".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Integer(1)),
+ );
+
+ let result = TypeSensitive::from_elicit_result_content(Some(content));
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_empty_string_array_when_missing_optional_vec() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Optional vec", mode = form)]
+ pub struct OptionalVec {
+ pub name: String,
+ pub hobbies: Option>,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "name".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "Charlie".to_string(),
+ )),
+ );
+ // hobbies omitted entirely
+
+ let user = OptionalVec::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Charlie");
+ assert_eq!(user.hobbies, None);
+}
+
+#[test]
+fn test_empty_content_map_becomes_default_values() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Defaults", mode = form)]
+ pub struct WithOptionals {
+ pub name: String,
+ pub age: i64,
+ pub is_admin: bool,
+ }
+
+ let result = WithOptionals::from_elicit_result_content(None);
+ assert!(result.is_err());
+
+ let result_empty = WithOptionals::from_elicit_result_content(Some(HashMap::new()));
+ assert!(result_empty.is_err());
+}
+
+#[test]
+fn test_boolean_handling() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Bool test", mode = form)]
+ pub struct BoolStruct {
+ pub is_active: bool,
+ pub has_permission: Option,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "is_active".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Boolean(
+ true,
+ )),
+ );
+ content.insert(
+ "has_permission".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Boolean(
+ false,
+ )),
+ );
+
+ let s = BoolStruct::from_elicit_result_content(Some(content)).unwrap();
+ assert!(s.is_active);
+ assert_eq!(s.has_permission, Some(false));
+}
+
+#[test]
+fn test_numeric_types_variations() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Numbers", mode = form)]
+ pub struct Numbers {
+ pub count: i32,
+ pub ratio: Option,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "count".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::Integer(42)),
+ );
+
+ let n = Numbers::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(n.count, 42);
+ assert_eq!(n.ratio, None);
+}
+
+#[test]
+fn test_url_mode_with_elicitation_id() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Go to this link", mode = url, url = "https://example.com/form/123")]
+ pub struct ExternalForm {
+ pub token: String,
+ }
+
+ let params = ExternalForm::elicit_url_params("elicit-999".to_string());
+ assert_eq!(params.elicitation_id, "elicit-999");
+ assert_eq!(params.message, "Go to this link");
+ assert_eq!(params.url, "https://example.com/form/123");
+
+ let req_params = ExternalForm::elicit_request_params("elicit-999".to_string());
+ match req_params {
+ ElicitRequestParams::UrlParams(p) => {
+ assert_eq!(p.elicitation_id, "elicit-999");
+ }
+ _ => panic!("Wrong variant"),
+ }
+}
+#[test]
+fn test_form_and_url_share_same_from_elicit_result_content_logic() {
+ // This ensures both modes reuse the same parsing logic (good!)
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Same parsing", mode = form)]
+ pub struct FormSame {
+ pub x: String,
+ }
+
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Same parsing", mode = url, url = "http://localhost")]
+ pub struct UrlSame {
+ pub x: String,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "x".to_string(),
+ ElicitResultContent::Primitive(rust_mcp_schema::ElicitResultContentPrimitive::String(
+ "shared".to_string(),
+ )),
+ );
+
+ let f = FormSame::from_elicit_result_content(Some(content.clone())).unwrap();
+ let u = UrlSame::from_elicit_result_content(Some(content)).unwrap();
+
+ assert_eq!(f.x, "shared");
+ assert_eq!(u.x, "shared");
+}
+
+#[test]
+fn test_string_array_empty_input_becomes_empty_vec() {
+ #[derive(Debug, Clone, JsonSchema)]
+ #[mcp_elicit(message = "Empty array", mode = form)]
+ pub struct EmptyArray {
+ pub items: Vec,
+ }
+
+ let mut content = HashMap::new();
+ content.insert(
+ "items".to_string(),
+ ElicitResultContent::StringArray(vec![]),
+ );
+
+ let s = EmptyArray::from_elicit_result_content(Some(content)).unwrap();
+ assert!(s.items.is_empty());
+}
+
+#[test]
+fn readme_example_elicitation() {
+ use rust_mcp_macros::{mcp_elicit, JsonSchema};
+ use rust_mcp_schema::{ElicitRequestParams, ElicitResultContent};
+ use std::collections::HashMap;
+
+ #[mcp_elicit(message = "Please enter your info", mode = form)]
+ #[derive(JsonSchema)]
+ pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
+ }
+
+ let params = UserInfo::elicit_request_params();
+ if let ElicitRequestParams::FormParams(form) = params {
+ assert_eq!(form.message, "Please enter your info");
+ }
+
+ // Simulate user input
+ let mut content: HashMap = HashMap::new();
+ content.insert("name".to_string(), "Alice".into());
+ content.insert("email".to_string(), "alice@Borderland.com".into());
+ content.insert("age".to_string(), 25.into());
+ content.insert("tags".to_string(), vec!["rust", "c++"].into());
+
+ let user = UserInfo::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Alice");
+ assert_eq!(user.age, 25);
+ assert_eq!(user.tags, vec!["rust", "c++"]);
+ assert_eq!(user.email.unwrap(), "alice@Borderland.com");
+}
+
+#[test]
+fn readme_example_elicitation_url() {
+ #[mcp_elicit(message = "Complete the form", mode = url, url = "https://example.com/form")]
+ #[derive(JsonSchema)]
+ pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
+ }
+
+ let elicit_url = UserInfo::elicit_url_params("elicit_10".into());
+
+ assert_eq!(elicit_url.message, "Complete the form");
+
+ // Simulate user input
+ let mut content: HashMap = HashMap::new();
+ content.insert("name".to_string(), "Alice".into());
+ content.insert("email".to_string(), "alice@Borderland.com".into());
+ content.insert("age".to_string(), 25.into());
+ content.insert("tags".to_string(), vec!["rust", "c++"].into());
+
+ let user = UserInfo::from_elicit_result_content(Some(content)).unwrap();
+ assert_eq!(user.name, "Alice");
+ assert_eq!(user.age, 25);
+ assert_eq!(user.tags, vec!["rust", "c++"]);
+ assert_eq!(user.email.unwrap(), "alice@Borderland.com");
+}
diff --git a/crates/rust-mcp-macros/tests/test_mcp_tool.rs b/crates/rust-mcp-macros/tests/test_mcp_tool.rs
new file mode 100644
index 0000000..1f4a213
--- /dev/null
+++ b/crates/rust-mcp-macros/tests/test_mcp_tool.rs
@@ -0,0 +1,477 @@
+#[macro_use]
+extern crate rust_mcp_macros;
+use common::EditOperation;
+use rust_mcp_macros::{mcp_elicit, JsonSchema};
+use rust_mcp_schema::{
+ CallToolRequestParams, ElicitRequestFormParams, ElicitRequestParams, ElicitResultContent,
+ ElicitResultContentPrimitive, RpcError,
+};
+use rust_mcp_schema::{IconTheme, Tool, ToolExecutionTaskSupport};
+use serde_json::json;
+
+#[path = "common/common.rs"]
+pub mod common;
+
+#[test]
+fn test_rename() {
+ let schema = EditOperation::json_schema();
+
+ assert_eq!(schema.len(), 3);
+
+ assert!(schema.contains_key("properties"));
+ assert!(schema.contains_key("required"));
+
+ assert!(schema.contains_key("type"));
+ assert_eq!(schema.get("type").unwrap(), "object");
+
+ let required: Vec<_> = schema
+ .get("required")
+ .unwrap()
+ .as_array()
+ .unwrap()
+ .iter()
+ .filter_map(|v| v.as_str())
+ .collect();
+
+ assert_eq!(required.len(), 2);
+ assert!(required.contains(&"oldText"));
+ assert!(required.contains(&"newText"));
+
+ 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 basic_tool_name_and_description() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(name = "echo", description = "Repeats input")]
+ struct Echo {
+ message: String,
+ }
+
+ let tool = Echo::tool();
+ assert_eq!(tool.name, "echo");
+ assert_eq!(tool.description.unwrap(), "Repeats input");
+}
+
+#[test]
+fn meta_json_is_parsed_correctly() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "weather",
+ description = "Get weather",
+ meta = r#"{"category": "utility", "version": "1.0"}"#
+ )]
+ struct Weather {
+ location: String,
+ }
+
+ let tool = Weather::tool();
+ let meta = tool.meta.as_ref().unwrap();
+ assert_eq!(meta["category"], "utility");
+ assert_eq!(meta["version"], "1.0");
+}
+
+#[test]
+fn title_is_set() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "calculator",
+ description = "Math tool",
+ title = "Scientific Calculator"
+ )]
+ struct Calc {
+ expression: String,
+ }
+
+ let tool = Calc::tool();
+ assert_eq!(tool.title.unwrap(), "Scientific Calculator");
+}
+
+#[test]
+fn all_annotations_are_set() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "delete_file",
+ description = "Deletes a file",
+ destructive_hint = true,
+ idempotent_hint = false,
+ open_world_hint = true,
+ read_only_hint = false
+ )]
+ struct DeleteFile {
+ path: String,
+ }
+
+ let tool = DeleteFile::tool();
+ let ann = tool.annotations.as_ref().unwrap();
+
+ assert!(ann.destructive_hint.unwrap());
+ assert!(!ann.idempotent_hint.unwrap());
+ assert!(ann.open_world_hint.unwrap());
+ assert!(!ann.read_only_hint.unwrap());
+}
+
+#[test]
+fn partial_annotations_some_set_some_not() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "get_user",
+ description = "Fetch user",
+ read_only_hint = true,
+ idempotent_hint = true
+ )]
+ struct GetUser {
+ id: String,
+ }
+
+ let tool = GetUser::tool();
+ let ann = tool.annotations.as_ref().unwrap();
+
+ assert!(ann.read_only_hint.unwrap());
+ assert!(ann.idempotent_hint.unwrap());
+ assert!(ann.destructive_hint.is_none());
+ assert!(ann.open_world_hint.is_none());
+}
+
+#[test]
+fn execution_task_support_required() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "long_task",
+ description = "desc",
+ execution(task_support = "required")
+ )]
+ struct LongTask {
+ data: String,
+ }
+
+ let tool = LongTask::tool();
+ let exec = tool.execution.as_ref().unwrap();
+ assert_eq!(exec.task_support, Some(ToolExecutionTaskSupport::Required));
+}
+
+#[test]
+fn execution_task_support_optional_and_forbidden() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "quick_op",
+ description = "description",
+ execution(task_support = "optional")
+ )]
+ struct QuickOp {
+ value: i32,
+ }
+
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "no_task",
+ description = "description",
+ execution(task_support = "forbidden")
+ )]
+ struct NoTask {
+ flag: bool,
+ }
+
+ assert_eq!(
+ QuickOp::tool().execution.unwrap().task_support,
+ Some(ToolExecutionTaskSupport::Optional)
+ );
+ assert_eq!(
+ NoTask::tool().execution.unwrap().task_support,
+ Some(ToolExecutionTaskSupport::Forbidden)
+ );
+}
+
+// #[derive(JsonSchema)]
+// #[mcp_tool(
+// name = "icon_tool",
+// icons = [
+// { src = "/icons/light.png", mime_type = "image/png", sizes = ["48x48", "96x96"], theme = "light" },
+// { src = "/icons/dark.svg", mime_type = "image/svg+xml", sizes = ["any"], theme = "dark" },
+// { src = "/icons/default.ico", sizes = ["32x32"] } // no mime/theme
+// ]
+// )]
+// struct IconTool {
+// input: String,
+// }
+
+#[test]
+fn icons_full_support() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "icon_tool",
+ description="desc",
+ icons = [
+ (src = "/icons/light.png", mime_type = "image/png", sizes = ["48x48", "96x96"], theme = "light" ),
+ ( src = "/icons/dark.svg", mime_type = "image/svg+xml", sizes = ["any"], theme = "dark" ),
+ ( src = "/icons/default.ico", sizes = ["32x32"] )
+ ]
+ )]
+ struct IconTool {
+ input: String,
+ }
+
+ let tool = IconTool::tool();
+ let icons = &tool.icons;
+
+ assert_eq!(icons.len(), 3);
+
+ assert_eq!(icons[0].src, "/icons/light.png");
+ assert_eq!(icons[0].mime_type.as_deref(), Some("image/png"));
+ assert_eq!(icons[0].sizes, vec!["48x48", "96x96"]);
+ assert_eq!(icons[0].theme, Some(IconTheme::Light));
+
+ assert_eq!(icons[1].src, "/icons/dark.svg");
+ assert_eq!(icons[1].mime_type.as_deref(), Some("image/svg+xml"));
+ assert_eq!(icons[1].sizes, vec!["any"]);
+ assert_eq!(icons[1].theme, Some(IconTheme::Dark));
+
+ assert_eq!(icons[2].src, "/icons/default.ico");
+ assert_eq!(icons[2].mime_type, None);
+ assert_eq!(icons[2].sizes, vec!["32x32"]);
+ assert_eq!(icons[2].theme, None);
+}
+
+#[test]
+fn icons_empty_when_not_provided() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(name = "no_icons", description = "no_icons")]
+ struct NoIcons {
+ _x: i32,
+ }
+ assert!(NoIcons::tool().icons.is_empty());
+}
+
+#[test]
+fn input_schema_has_correct_required_fields() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(name = "user_create", description = "user_create")]
+ struct UserCreate {
+ username: String,
+ email: String,
+ age: Option,
+ tags: Vec,
+ }
+
+ let tool: Tool = UserCreate::tool();
+ let required = tool.input_schema.required;
+ assert!(required.contains(&"username".to_string()));
+ assert!(required.contains(&"email".to_string()));
+ assert!(required.contains(&"tags".to_string()));
+ assert!(!required.contains(&"age".to_string()));
+}
+
+#[test]
+fn properties_are_correctly_mapped() {
+ #[allow(unused)]
+ #[derive(JsonSchema)]
+ #[mcp_tool(name = "test_props", description = "test_props")]
+ struct TestProps {
+ name: String,
+ count: i32,
+ active: bool,
+ score: Option,
+ }
+
+ let tool: Tool = TestProps::tool();
+ let schema = tool.input_schema;
+ let props = schema.properties.unwrap();
+
+ assert!(props.contains_key("name"));
+ assert!(props.contains_key("count"));
+ assert!(props.contains_key("active"));
+ assert!(props.contains_key("score"));
+
+ let name_prop = props.get("name").unwrap();
+ assert_eq!(name_prop.get("type").unwrap().as_str().unwrap(), "string");
+
+ let active_prop = props.get("active").unwrap();
+ assert_eq!(
+ active_prop.get("type").unwrap().as_str().unwrap(),
+ "boolean"
+ );
+}
+
+#[test]
+fn tool_name_fallback_when_not_provided() {
+ #[derive(JsonSchema)]
+ #[mcp_tool(name = "fallback-name-tool", description = "No name, uses struct name")]
+ struct FallbackNameTool {
+ input: String,
+ }
+
+ let tool: Tool = FallbackNameTool::tool();
+ assert_eq!(tool.name, "fallback-name-tool"); // Uses struct name
+}
+
+#[test]
+fn meta_is_ignored_when_feature_off() {
+ // Should compile even if meta is provided
+ #[derive(JsonSchema)]
+ #[mcp_tool(
+ name = "old_schema",
+ description = "old_schema",
+ meta = r#"{"ignored": true}"#
+ )]
+ struct OldTool {
+ x: i32,
+ }
+
+ let tool: Tool = OldTool::tool();
+
+ assert_eq!(tool.name, "old_schema");
+ let meta = tool.meta.unwrap();
+ assert_eq!(meta, json!({"ignored": true}).as_object().unwrap().clone());
+}
+
+#[test]
+fn readme_example_tool() {
+ #[mcp_tool(
+ name = "write_file",
+ title = "Write File Tool",
+ description = "Create or overwrite a file with content.",
+ destructive_hint = false,
+ idempotent_hint = false,
+ open_world_hint = false,
+ read_only_hint = false,
+ execution(task_support = "optional"),
+ icons = [
+ (src = "https:/mywebsite.com/write.png", mime_type = "image/png", sizes = ["128x128"], theme = "light"),
+ (src = "https:/mywebsite.com/write_dark.svg", mime_type = "image/svg+xml", sizes = ["64x64","128x128"], theme = "dark")
+ ],
+ meta = r#"{"key": "value"}"#
+ )]
+ #[derive(JsonSchema)]
+ pub struct WriteFileTool {
+ /// The target file's path.
+ pub path: String,
+ /// The string content to be written to the file
+ pub content: String,
+ }
+
+ assert_eq!(WriteFileTool::tool_name(), "write_file");
+
+ let tool: rust_mcp_schema::Tool = WriteFileTool::tool();
+ assert_eq!(tool.name, "write_file");
+ assert_eq!(tool.title.as_ref().unwrap(), "Write File Tool");
+ assert_eq!(
+ tool.description.unwrap(),
+ "Create or overwrite a file with content."
+ );
+
+ let icons = tool.icons;
+ assert_eq!(icons.len(), 2);
+ assert_eq!(icons[0].src, "https:/mywebsite.com/write.png");
+ assert_eq!(icons[0].mime_type, Some("image/png".into()));
+ assert_eq!(icons[0].theme, Some("light".into()));
+ assert_eq!(icons[0].sizes, vec!["128x128"]);
+ assert_eq!(icons[1].mime_type, Some("image/svg+xml".into()));
+
+ let meta: &serde_json::Map = tool.meta.as_ref().unwrap();
+ assert_eq!(
+ meta.get("key").unwrap(),
+ &serde_json::Value::String("value".to_string())
+ );
+
+ let schema_properties = tool.input_schema.properties.unwrap();
+ assert_eq!(schema_properties.len(), 2);
+ assert!(schema_properties.contains_key("path"));
+ assert!(schema_properties.contains_key("content"));
+
+ // get the `content` prop from schema
+ let content_prop = schema_properties.get("content").unwrap();
+
+ // assert the type
+ assert_eq!(content_prop.get("type").unwrap(), "string");
+ // assert the description
+ assert_eq!(
+ content_prop.get("description").unwrap(),
+ "The string content to be written to the file"
+ );
+
+ let request_params = WriteFileTool::request_params().with_arguments(
+ json!({"path":"./test.txt","content":"hello tool"})
+ .as_object()
+ .unwrap()
+ .clone(),
+ );
+
+ assert_eq!(request_params.name, "write_file");
+}
diff --git a/crates/rust-mcp-sdk/Cargo.toml b/crates/rust-mcp-sdk/Cargo.toml
index 609b0ac..7652841 100644
--- a/crates/rust-mcp-sdk/Cargo.toml
+++ b/crates/rust-mcp-sdk/Cargo.toml
@@ -66,8 +66,7 @@ default = [
"sse",
"streamable-http",
"hyper-server",
- "ssl",
- "2025_06_18",
+ "ssl"
] # All features enabled by default
sse = ["rust-mcp-transport/sse","http","http-body","http-body-util"]
@@ -82,36 +81,6 @@ 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 = [
- "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 = [
- "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 = [
- "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 d92d964..715f280 100644
--- a/crates/rust-mcp-sdk/README.md
+++ b/crates/rust-mcp-sdk/README.md
@@ -11,26 +11,21 @@
[
](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!
-**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem.
-Leveraging the [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) crate simplifies the process of building robust and reliable MCP servers and clients, ensuring consistency and minimizing errors in data handling and message processing.
+A high-performance, asynchronous Rust toolkit for building MCP servers and clients.
+Focus on your application logic - rust-mcp-sdk handles the protocol, transports, and the rest!
+This SDK fully implements the latest MCP protocol version ([2025-11-25](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema)), with backward compatibility built-in. `rust-mcp-sdk` provides the necessary components for developing both servers and clients in the MCP ecosystem. It leverages the [rust-mcp-schema](https://crates.io/crates/rust-mcp-schema) crate for type-safe schema objects and includes powerful procedural macros for tools and user input elicitation.
-**rust-mcp-sdk** supports all three official versions of the MCP protocol.
-By default, it uses the **2025-06-18** version, but earlier versions can be enabled via Cargo features.
-
-π 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.
-
-
-**Features**
-- β
Stdio, SSE and Streamable HTTP Support
-- β
Supports multiple MCP protocol versions
+**Key Features**
+- β
Latest MCP protocol specification supported: 2025-11-25
+- β
Transports:Stdio, Streamable HTTP, and backward-compatible SSE support
+- β
Lightweight Axum-based server for Streamable HTTP and SSE
+- β
Multi-client concurrency
- β
DNS Rebinding Protection
+- β
Resumability
- β
Batch Messages
- β
Streaming & non-streaming JSON response
-- β
Resumability
- β
OAuth Authentication for MCP Servers
- β
[Remote Oauth Provider](crates/rust-mcp-sdk/src/auth/auth_provider/remote_auth_provider.rs) (for any provider with DCR support)
- β
**Keycloak** Provider (via [rust-mcp-extra](crates/rust-mcp-extra/README.md#keycloak))
@@ -41,24 +36,26 @@ By default, it uses the **2025-06-18** version, but earlier versions can be enab
**β οΈ** Project is currently under development and should be used at your own risk.
## Table of Contents
-- [Getting Started](#getting-started)
+- [Quick Start](#quick-start)
+ - [Minimal MCP Server (Stdio)]([#minimal-mcp-server-stdio](#minimal-mcp-server-stdio))
+ - [Minimal MCP Server (Streamable HTTP)](#minimal-mcp-server-streamable-http)
+ - [Minimal MCP Client (Stdio)](#minimal-mcp-client-stdio)
- [Usage Examples](#usage-examples)
- - [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)
-- [Authentication](#authentication)
- [Macros](#macros)
+ - [mcp_tool](#mcp_tool)
+ - [tool_box](#-tool_box)
+ - [mcp_icon](#-mcp_icon)
+- [Authentication](#authentication)
+ - [RemoteAuthProvider](#remoteauthprovider)
+ - [OAuthProxy](#oauthproxy)
- [HyperServerOptions](#hyperserveroptions)
- - [Security Considerations](#security-considerations)
+- [Security Considerations](#security-considerations)
- [Cargo features](#cargo-features)
- [Available Features](#available-features)
- - [MCP protocol versions with corresponding features](#mcp-protocol-versions-with-corresponding-features)
- [Default Features](#default-features)
- [Using Only the server Features](#using-only-the-server-features)
- [Using Only the client Features](#using-only-the-client-features)
-- [Choosing Between Standard and Core Handlers traits](#choosing-between-standard-and-core-handlers-traits)
+- [Handler Traits](#handlers-traits)
- [Choosing Between **ServerHandler** and **ServerHandlerCore**](#choosing-between-serverhandler-and-serverhandlercore)
- [Choosing Between **ClientHandler** and **ClientHandlerCore**](#choosing-between-clienthandler-and-clienthandlercore)
- [Projects using Rust MCP SDK](#projects-using-rust-mcp-sdk)
@@ -67,330 +64,339 @@ By default, it uses the **2025-06-18** version, but earlier versions can be enab
- [License](#license)
-## Getting Started
-If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md)
-## Usage Examples
+## Quick Start
-### MCP Server (stdio)
+
-Create a MCP server with a `tool` that will print a `Hello World!` message:
+Add to your Cargo.toml:
+```toml
+[dependencies]
+rust-mcp-sdk = "0.9.0" # Check crates.io for the latest version
+```
+
+
+
+## Minimal MCP Server (Stdio)
+```rs
+use async_trait::async_trait;
+use rust_mcp_sdk::{*,error::SdkResult,macros,mcp_server::{server_runtime, ServerHandler},schema::*,};
+
+// Define a mcp tool
+#[macros::mcp_tool(name = "say_hello", description = "returns \"Hello from Rust MCP SDK!\" message ")]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
+pub struct SayHelloTool {}
+
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler;
+
+// implement ServerHandler
+#[async_trait]
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {
+ tools: vec![SayHelloTool::tool()],
+ meta: None,
+ next_cursor: None,
+ })
+ }
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(&self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {
+ Ok(CallToolResult::text_content(vec!["Hello from Rust MCP SDK!".into()]))
+ } else {
+ Err(CallToolError::unknown_tool(params.name))
+ }
+ }
+}
-```rust
#[tokio::main]
async fn main() -> SdkResult<()> {
-
- // STEP 1: Define server details and capabilities
- let server_details = InitializeResult {
- // server name and version
+ // Define server details and capabilities
+ let server_info = InitializeResult {
server_info: Implementation {
- name: "Hello World MCP Server".to_string(),
- version: "0.1.0".to_string(),
- title: Some("Hello World MCP Server".to_string()),
- },
- capabilities: ServerCapabilities {
- // indicates that server support mcp tools
- tools: Some(ServerCapabilitiesTools { list_changed: None }),
- ..Default::default() // Using default values for other fields
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
},
- meta: None,
- instructions: Some("server instructions...".to_string()),
- protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
};
- // STEP 2: create a std transport with default options
let transport = StdioTransport::new(TransportOptions::default())?;
-
- // STEP 3: instantiate our custom handler for handling MCP messages
- let handler = MyServerHandler {};
-
- // STEP 4: create a MCP server
- let server: ServerRuntime = server_runtime::create_server(server_details, transport, handler);
-
- // STEP 5: Start the server
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = server_runtime::create_server(server_info, transport, handler);
server.start().await
-
}
```
-See hello-world-mcp-server-stdio example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
-
-
-
-### MCP Server (Streamable HTTP)
-
-Creating an MCP server in `rust-mcp-sdk` with the `sse` transport allows multiple clients to connect simultaneously with no additional setup.
-Simply create a Hyper Server using `hyper_server::create_server()` and pass in the same handler and HyperServerOptions.
-
-
-π‘ By default, both **Streamable HTTP** and **SSE** transports are enabled for backward compatibility. To disable the SSE transport , set the `sse_support` to false in the `HyperServerOptions`.
-
+## Minimal MCP Server (Streamable HTTP)
+Creating an MCP server in `rust-mcp-sdk` allows multiple clients to connect simultaneously with no additional setup.
+The setup is nearly identical to the stdio example shown above. You only need to create a Hyper server via `hyper_server::create_server()` and pass in the same handler and `HyperServerOptions`.
+π‘ If backward compatibility is required, you can enable **SSE** transport by setting `sse_support` to true in `HyperServerOptions`.
```rust
-
-// STEP 1: Define server details and capabilities
-let server_details = InitializeResult {
- // server name and version
- server_info: Implementation {
- name: "Hello World MCP Server".to_string(),
- version: "0.1.0".to_string(),
- title: Some("Hello World MCP Server".to_string()),
- },
- capabilities: ServerCapabilities {
- // indicates that server support mcp tools
- tools: Some(ServerCapabilitiesTools { list_changed: None }),
- ..Default::default() // Using default values for other fields
- },
- meta: None,
- instructions: Some("server instructions...".to_string()),
- protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
+use async_trait::async_trait;
+use rust_mcp_sdk::{*,error::SdkResult,event_store::InMemoryEventStore,macros,
+ mcp_server::{hyper_server, HyperServerOptions, ServerHandler},schema::*,
};
-// STEP 2: instantiate our custom handler for handling MCP messages
-let handler = MyServerHandler {};
-
-// STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions
-let server = hyper_server::create_server(
- server_details,
- handler,
- HyperServerOptions {
- host: "127.0.0.1".to_string(),
- sse_support: false,
- event_store: Some(Arc::new(InMemoryEventStore::default())), // enable resumability
- ..Default::default()
- },
-);
-
-// STEP 4: Start the server
-server.start().await?;
-
-Ok(())
-```
-
-
-The implementation of `MyServerHandler` is the same regardless of the transport used and could be as simple as the following:
-
-```rust
-
-// STEP 1: Define a rust_mcp_schema::Tool ( we need one with no parameters for this example)
-#[mcp_tool(name = "say_hello_world", description = "Prints \"Hello World!\" message")]
-#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+// Define a mcp tool
+#[macros::mcp_tool(
+ name = "say_hello",
+ description = "returns \"Hello from Rust MCP SDK!\" message "
+)]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
pub struct SayHelloTool {}
-// STEP 2: Implement ServerHandler trait for a custom handler
-// For this example , we only need handle_list_tools_request() and handle_call_tool_request() methods.
-pub struct MyServerHandler;
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler;
+// implement ServerHandler
#[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: Arc) -> Result {
-
- Ok(ListToolsResult {
- tools: vec![SayHelloTool::tool()],
- meta: None,
- next_cursor: None,
- })
-
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {tools: vec![SayHelloTool::tool()],meta: None,next_cursor: None})
}
-
- /// Handles requests to call a specific tool.
- 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())] ))
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(
+ &self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {Ok(CallToolResult::text_content(vec!["Hello from Rust MCP SDK!".into()]))
} else {
- Err(CallToolError::unknown_tool(request.tool_name().to_string()))
+ Err(CallToolError::unknown_tool(params.name))
}
-
}
}
-```
-
----
-
-π 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) :
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+ // Define server details and capabilities
+ let server_info = InitializeResult {
+ server_info: Implementation {
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
+ },
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
+ };
-
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = hyper_server::create_server(
+ server_info,
+ handler,
+ HyperServerOptions {
+ host: "127.0.0.1".to_string(),
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
+ ..Default::default()
+ },
+ );
+ server.start().await?;
+ Ok(())
+}
+```
----
-### MCP Client (stdio)
+## Minimal MCP Client (Stdio)
+Following is implementation of an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools provided by the server.
-Create an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools, then uses the add tool provided by the server to sum 120 and 28, printing the result.
```rust
+use async_trait::async_trait;
+use rust_mcp_sdk::{*, error::SdkResult,
+ mcp_client::{client_runtime, ClientHandler},
+ schema::*,
+};
-// STEP 1: Custom Handler to handle incoming MCP Messages
+// 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
+ // To see all the trait methods you can override,
+ // check out:
+ // 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
+ // Client details and capabilities
let client_details: InitializeRequestParams = InitializeRequestParams {
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "simple-rust-mcp-client".into(),
version: "0.1.0".into(),
+ description: None,
+ icons: vec![],
+ title: None,
+ website_url: None,
},
- protocol_version: LATEST_PROTOCOL_VERSION.into(),
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ meta: None,
};
- // Step3 : Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server
+ // Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server
let transport = StdioTransport::create_with_server_launch(
- "npx",
- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()],
- None, TransportOptions::default()
+ "npx",vec!["-y".to_string(),"@modelcontextprotocol/server-everything@latest".to_string()],
+ None,
+ TransportOptions::default(),
)?;
- // STEP 4: instantiate our custom handler for handling MCP messages
+ // instantiate our custom handler for handling MCP messages
let handler = MyClientHandler {};
- // STEP 5: create a MCP client
- let client = client_runtime::create_client(client_details, transport, handler);
-
- // STEP 6: start the MCP client
+ // Create and start the MCP client
+ let client = client_runtime::create_client(client_details, transport, handler);
client.clone().start().await?;
+ // use client methods to communicate with the MCP Server as you wish:
- // STEP 7: use client methods to communicate with the MCP Server as you wish
-
+ let server_version = client.server_version().unwrap();
+
// 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);
-
+ let tools = client.request_tool_list(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!(" {}. {} : {}", 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(())
}
-
```
-Here is the output :
+## Usage Examples
-
+π For full examples (stdio, Streamable HTTP, clients, auth, etc.), see the [examples/](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples) directory.
-> your results may vary slightly depending on the version of the MCP Server in use when you run it.
+π If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md)
-### MCP Client (Streamable HTTP)
-```rs
+See [hello-world-mcp-server-stdio](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio) example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) :
-// 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<()> {
+## Macros
+Enable with the `macros` feature.
- // 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(),
- };
+[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.
- // 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()
- },
- };
+### βΎ`mcp_tool`
+Generate a [Tool](https://docs.rs/rust-mcp-schema/latest/rust_mcp_schema/struct.Tool.html) from a struct, with rich metadata (icons, execution hints, etc.).
- // STEP 4: instantiate the custom handler that is responsible for handling MCP messages
- let handler = MyClientHandler {};
+example usage:
+```rs
+#[mcp_tool(
+ name = "write_file",
+ title = "Write File Tool",
+ description = "Create a new file or completely overwrite an existing file with new content.",
+ destructive_hint = false idempotent_hint = false open_world_hint = false read_only_hint = false,
+ meta = r#"{ "key" : "value", "string_meta" : "meta value", "numeric_meta" : 15}"#,
+ execution(task_support = "optional"),
+ icons = [(src = "https:/website.com/write.png", mime_type = "image/png", sizes = ["128x128"], theme = "light")]
+)]
+#[derive(rust_mcp_macros::JsonSchema)]
+pub struct WriteFileTool {
+ /// The target file's path for writing content.
+ pub path: String,
+ /// The string content to be written to the file
+ pub content: String,
+}
+```
- // STEP 5: create the client with transport options and the handler
- let client = client_runtime::with_transport_options(client_details, transport_options, handler);
+π For complete documentation, example usage, and a list of all available attributes, please refer to https://crates.io/crates/rust-mcp-macros.
- // STEP 6: start the MCP client
- client.clone().start().await?;
+### βΎ `tool_box!()`
+Automatically generates an enum based on the provided list of tools, making it easier to organize and manage them, especially when your application includes a large number of tools.
- // STEP 7: use client methods to communicate with the MCP Server as you wish
+```rs
+tool_box!(GreetingTools, [SayHelloTool, SayGoodbyeTool]);
- // 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);
+let tools: Vec = GreetingTools::tools();
+``
- tools.iter().enumerate().for_each(|(tool_index, tool)| {
- println!(" {}. {} : {}",
- tool_index + 1,
- tool.name,
- tool.description.clone().unwrap_or_default()
- );
- });
+π» For a real-world example, check out [tools/](https://github.com/rust-mcp-stack/rust-mcp-filesystem/tree/main/src/tools) and
+[handle_call_tool_request(...)](https://github.com/rust-mcp-stack/rust-mcp-filesystem/blob/main/src/handler.rs#L195) in [rust-mcp-filesystem](https://github.com/rust-mcp-stack/rust-mcp-filesystem) project
- 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)};
+### βΎ [mcp_elicit](https://crates.io/crates/rust-mcp-macros)
+Generates type-safe elicitation (Form or URL mode) for user input.
- // invoke the tool
- let result = client.call_tool(request).await?;
+example usage:
+```rs
+#[mcp_elicit(message = "Please enter your info", mode = form)]
+#[derive(JsonSchema)]
+pub struct UserInfo {
+ #[json_schema(title = "Name", min_length = 5, max_length = 100)]
+ pub name: String,
+ #[json_schema(title = "Email", format = "email")]
+ pub email: Option,
+ #[json_schema(title = "Age", minimum = 15, maximum = 125)]
+ pub age: i32,
+ #[json_schema(title = "Tags")]
+ pub tags: Vec,
+}
- println!("{}",result.content.first().unwrap().as_text_content()?.text);
+// Sends a request to the client asking the user to provide input
+let result: ElicitResult = server.request_elicitation(UserInfo::elicit_request_params()).await?;
- client.shut_down().await?;
+// Convert result.content into a UserInfo instance
+let user_info = UserInfo::from_elicit_result_content(result.content)?;
- Ok(())
+println!("name: {}", user_info.name);
+println!("age: {}", user_info.age);
+println!("email: {}",user.email.clone().unwrap_or("not provider".into()));
+println!("tags: {}", user_info.tags.join(","));
```
-π 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.
+π For complete documentation, example usage, and a list of all available attributes, please refer to https://crates.io/crates/rust-mcp-macros.
+### βΎ `mcp_icon!()`
+A convenient icon builder for implementations and tools, offering full attribute support including theme, size, mime, and more.
-### MCP Client (sse)
-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(
-- "npx",
-- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()],
-- None, TransportOptions::default()
--)?;
-+ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?;
+example usage:
+```rs
+let icon: crate::schema::Icon = mcp_icon!(
+ src = "http://website.com/icon.png",
+ mime_type = "image/png",
+ sizes = ["64x64"],
+ theme = "dark"
+ );
```
-π 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.
-
-
## Authentication
MCP server can verify tokens issued by other systems, integrate with external identity providers, or manage the entire authentication process itself. Each option offers a different balance of simplicity, security, and control.
@@ -404,120 +410,12 @@ MCP server can verify tokens issued by other systems, integrate with external id
- [WorkOS autn example](crates/rust-mcp-extra/README.md#workos-authkit)
-
### OAuthProxy
OAuthProxy enables authentication with OAuth providers that donβt support Dynamic Client Registration (DCR).It accepts any client registration request, handles the DCR on your server side and then uses your pre-registered app credentials upstream.The proxy also forwards callbacks, allowing dynamic redirect URIs to work with providers that require fixed ones.
> β οΈ OAuthProxy support is still in development, please use RemoteAuthProvider for now.
-
-
-## 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
## HyperServerOptions
@@ -531,89 +429,22 @@ A typical example of creating a HyperServer that exposes the MCP server via Stre
let server = hyper_server::create_server(
server_details,
- handler,
+ handler.to_mcp_server_handler(),
HyperServerOptions {
host: "127.0.0.1".to_string(),
- enable_ssl: true,
+ port: 8080,
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
+ auth: Some(Arc::new(auth_provider)), // enable authentication
+ sse_support: false,
..Default::default()
},
);
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,
-
- /// 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.
- /// Default is false (SSE streams are preferred).
- pub enable_json_response: Option,
-
- /// Interval between automatic ping messages sent to clients to detect disconnects
- pub ping_interval: Duration,
-
- /// Enables SSL/TLS if set to `true`
- pub enable_ssl: bool,
-
- /// Path to the SSL/TLS certificate file (e.g., "cert.pem").
- /// Required if `enable_ssl` is `true`.
- pub ssl_cert_path: Option,
-
- /// Path to the SSL/TLS private key file (e.g., "key.pem").
- /// Required if `enable_ssl` is `true`.
- pub ssl_key_path: Option,
+π Refer to [HyperServerOptions](https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/hyper_servers/server.rs#L43) for a complete overview of HyperServerOptions attributes and options.
- /// List of allowed host header values for DNS rebinding protection.
- /// If not specified, host validation is disabled.
- pub allowed_hosts: Option>,
-
- /// List of allowed origin header values for DNS rebinding protection.
- /// If not specified, origin validation is disabled.
- pub allowed_origins: Option>,
-
- /// 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,
-
- /// Optional authentication provider for protecting MCP server.
- pub auth: Option>,
-}
-
-```
### Security Considerations
@@ -637,28 +468,19 @@ The `rust-mcp-sdk` crate provides several features that can be enabled or disabl
- `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
-
-- `2025_06_18` : Activates MCP Protocol version 2025-06-18 (enabled by default)
-- `2025_03_26` : Activates MCP Protocol version 2025-03-26
-- `2024_11_05` : Activates MCP Protocol version 2024-11-05
-
-> Note: MCP protocol versions are mutually exclusive-only one can be active at any given time.
-
### Default Features
-When you add rust-mcp-sdk as a dependency without specifying any features, all features are included, with the latest MCP Protocol version enabled by default:
+When you add rust-mcp-sdk as a dependency without specifying any features, all features are enabled by default
```toml
[dependencies]
-rust-mcp-sdk = "0.2.0"
+rust-mcp-sdk = "0.9.0"
```
diff --git a/crates/rust-mcp-sdk/examples/quick_start.rs b/crates/rust-mcp-sdk/examples/quick_start.rs
new file mode 100644
index 0000000..dfc8999
--- /dev/null
+++ b/crates/rust-mcp-sdk/examples/quick_start.rs
@@ -0,0 +1,77 @@
+use async_trait::async_trait;
+use rust_mcp_sdk::{
+ error::SdkResult,
+ macros,
+ mcp_server::{server_runtime, ServerHandler},
+ schema::*,
+ *,
+};
+
+// Define a mcp tool
+#[macros::mcp_tool(
+ name = "say_hello",
+ description = "returns \"Hello from Rust MCP SDK!\" message "
+)]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
+pub struct SayHelloTool {}
+
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler {}
+
+// implement ServerHandler
+#[async_trait]
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {
+ tools: vec![SayHelloTool::tool()],
+ meta: None,
+ next_cursor: None,
+ })
+ }
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(
+ &self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {
+ Ok(CallToolResult::text_content(vec![
+ "Hello from Rust MCP SDK!".into(),
+ ]))
+ } else {
+ Err(CallToolError::unknown_tool(params.name))
+ }
+ }
+}
+
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+ let server_info = InitializeResult {
+ server_info: Implementation {
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
+ },
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
+ };
+
+ let transport = StdioTransport::new(TransportOptions::default())?;
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = server_runtime::create_server(server_info, transport, handler);
+ server.start().await
+}
diff --git a/crates/rust-mcp-sdk/examples/quick_start_client_stdio.rs b/crates/rust-mcp-sdk/examples/quick_start_client_stdio.rs
new file mode 100644
index 0000000..377fe4e
--- /dev/null
+++ b/crates/rust-mcp-sdk/examples/quick_start_client_stdio.rs
@@ -0,0 +1,92 @@
+use async_trait::async_trait;
+use rust_mcp_sdk::{
+ error::SdkResult,
+ mcp_client::{client_runtime, ClientHandler},
+ schema::*,
+ *,
+};
+
+// Custom Handler to handle incoming MCP Messages
+pub struct MyClientHandler;
+#[async_trait]
+impl ClientHandler for MyClientHandler {
+ // To see all the trait methods you can override,
+ // check out:
+ // 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<()> {
+ // Client details and capabilities
+ let client_details: InitializeRequestParams = InitializeRequestParams {
+ capabilities: ClientCapabilities::default(),
+ client_info: Implementation {
+ name: "simple-rust-mcp-client".into(),
+ version: "0.1.0".into(),
+ description: None,
+ icons: vec![],
+ title: None,
+ website_url: None,
+ },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ meta: None,
+ };
+
+ // Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server
+ let transport = StdioTransport::create_with_server_launch(
+ "npx",
+ vec![
+ "-y".to_string(),
+ "@modelcontextprotocol/server-everything@latest".to_string(),
+ ],
+ None,
+ TransportOptions::default(),
+ )?;
+
+ // instantiate our custom handler for handling MCP messages
+ let handler = MyClientHandler {};
+
+ // Create and start the MCP client
+ let client = client_runtime::create_client(client_details, transport, handler);
+ client.clone().start().await?;
+
+ // use client methods to communicate with the MCP Server as you wish:
+
+ let server_version = client.server_version().unwrap();
+
+ // Retrieve and display the list of tools available on the server
+ let tools = client.request_tool_list(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 ...");
+ let params = serde_json::json!({"a": 100,"b": 28})
+ .as_object()
+ .unwrap()
+ .clone();
+ let request = CallToolRequestParams {
+ name: "add".to_string(),
+ arguments: Some(params),
+ meta: None,
+ task: None,
+ };
+ // invoke the tool
+ let result = client.request_tool_call(request).await?;
+ println!(
+ "{}",
+ result.content.first().unwrap().as_text_content()?.text
+ );
+
+ client.shut_down().await?;
+ Ok(())
+}
diff --git a/crates/rust-mcp-sdk/examples/quick_start_streamable_http.rs b/crates/rust-mcp-sdk/examples/quick_start_streamable_http.rs
new file mode 100644
index 0000000..bb9b5ca
--- /dev/null
+++ b/crates/rust-mcp-sdk/examples/quick_start_streamable_http.rs
@@ -0,0 +1,87 @@
+use async_trait::async_trait;
+use rust_mcp_sdk::{
+ error::SdkResult,
+ event_store::InMemoryEventStore,
+ macros,
+ mcp_server::{hyper_server, HyperServerOptions, ServerHandler},
+ schema::*,
+ *,
+};
+
+// Define a mcp tool
+#[macros::mcp_tool(
+ name = "say_hello",
+ description = "returns \"Hello from Rust MCP SDK!\" message "
+)]
+#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, macros::JsonSchema)]
+pub struct SayHelloTool {}
+
+// define a custom handler
+#[derive(Default)]
+struct HelloHandler {}
+
+// implement ServerHandler
+#[async_trait]
+impl ServerHandler for HelloHandler {
+ // Handles requests to list available tools.
+ async fn handle_list_tools_request(
+ &self,
+ _request: Option,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ Ok(ListToolsResult {
+ tools: vec![SayHelloTool::tool()],
+ meta: None,
+ next_cursor: None,
+ })
+ }
+ // Handles requests to call a specific tool.
+ async fn handle_call_tool_request(
+ &self,
+ params: CallToolRequestParams,
+ _runtime: std::sync::Arc,
+ ) -> std::result::Result {
+ if params.name == "say_hello" {
+ Ok(CallToolResult::text_content(vec![
+ "Hello from Rust MCP SDK!".into(),
+ ]))
+ } else {
+ Err(CallToolError::unknown_tool(params.name))
+ }
+ }
+}
+
+#[tokio::main]
+async fn main() -> SdkResult<()> {
+ // Define server details and capabilities
+ let server_info = InitializeResult {
+ server_info: Implementation {
+ name: "hello-rust-mcp".into(),
+ version: "0.1.0".into(),
+ title: Some("Hello World MCP Server".into()),
+ description: Some("A minimal Rust MCP server".into()),
+ icons: vec![mcp_icon!(src = "https://raw.githubusercontent.com/rust-mcp-stack/rust-mcp-sdk/main/assets/rust-mcp-icon.png",
+ mime_type = "image/png",
+ sizes = ["128x128"],
+ theme = "light")],
+ website_url: Some("https://github.com/rust-mcp-stack/rust-mcp-sdk".into()),
+ },
+ capabilities: ServerCapabilities { tools: Some(ServerCapabilitiesTools { list_changed: None }), ..Default::default() },
+ protocol_version: ProtocolVersion::V2025_11_25.into(),
+ instructions: None,
+ meta:None
+ };
+
+ let handler = HelloHandler::default().to_mcp_server_handler();
+ let server = hyper_server::create_server(
+ server_info,
+ handler,
+ HyperServerOptions {
+ host: "127.0.0.1".to_string(),
+ event_store: Some(std::sync::Arc::new(InMemoryEventStore::default())), // enable resumability
+ ..Default::default()
+ },
+ );
+ server.start().await?;
+ Ok(())
+}
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 5cedb59..4b41592 100644
--- a/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs
+++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs
@@ -1,30 +1,33 @@
use std::{sync::Arc, time::Duration};
+use crate::{
+ error::SdkResult,
+ mcp_server::{
+ error::{TransportServerError, TransportServerResult},
+ ServerRuntime,
+ },
+};
use crate::{
mcp_http::McpAppState,
mcp_server::HyperServer,
schema::{
schema_utils::{NotificationFromServer, RequestFromServer, ResultFromClient},
- CreateMessageRequestParams, CreateMessageResult, InitializeRequestParams,
- ListRootsRequestParams, ListRootsResult, LoggingMessageNotificationParams,
- PromptListChangedNotificationParams, ResourceListChangedNotificationParams,
- ResourceUpdatedNotificationParams, ToolListChangedNotificationParams,
+ CreateMessageRequestParams, CreateMessageResult, InitializeRequestParams, ListRootsResult,
+ LoggingMessageNotificationParams, NotificationParams, RequestParams,
+ ResourceUpdatedNotificationParams,
},
McpServer,
};
-
use axum_server::Handle;
+use rust_mcp_schema::{
+ schema_utils::{CustomNotification, CustomRequest},
+ CancelTaskParams, CancelTaskResult, CancelledNotificationParams, ElicitCompleteParams,
+ ElicitRequestParams, ElicitResult, GenericResult, GetTaskParams, GetTaskPayloadParams,
+ GetTaskResult, ProgressNotificationParams, TaskStatusNotificationParams,
+};
use rust_mcp_transport::SessionId;
use tokio::task::JoinHandle;
-use crate::{
- error::SdkResult,
- mcp_server::{
- error::{TransportServerError, TransportServerResult},
- ServerRuntime,
- },
-};
-
pub struct HyperRuntime {
pub(crate) state: Arc,
pub(crate) server_task: JoinHandle>,
@@ -85,6 +88,11 @@ impl HyperRuntime {
)
}
+ /// Sends a request to the client and processes the response.
+ ///
+ /// This function sends a `RequestFromServer` message to the client, waits for the response,
+ /// and handles the result. If the response is empty or of an invalid type, an error is returned.
+ /// Otherwise, it returns the result from the client.
pub async fn send_request(
&self,
session_id: &SessionId,
@@ -104,115 +112,317 @@ impl HyperRuntime {
runtime.send_notification(notification).await
}
+ pub async fn client_info(
+ &self,
+ session_id: &SessionId,
+ ) -> SdkResult