diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 9963d87..3e0a4c9 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -49,6 +49,7 @@ jobs: echo "Updating root Cargo.toml" sed -i -e "s/rust-mcp-macros = { version = \"[^\"]*\",/rust-mcp-macros = { version = \"$(grep '^version = ' crates/rust-mcp-macros/Cargo.toml | cut -d' ' -f3 | tr -d '\"')\",/" \ -e "s/rust-mcp-transport = { version = \"[^\"]*\",/rust-mcp-transport = { version = \"$(grep '^version = ' crates/rust-mcp-transport/Cargo.toml | cut -d' ' -f3 | tr -d '\"')\",/" \ + -e "s/rust-mcp-extra = { version = \"[^\"]*\",/rust-mcp-extra = { version = \"$(grep '^version = ' crates/rust-mcp-extra/Cargo.toml | cut -d' ' -f3 | tr -d '\"')\",/" \ Cargo.toml git add ./Cargo.toml diff --git a/.release-config.json b/.release-config.json index fecd10a..304ccc9 100644 --- a/.release-config.json +++ b/.release-config.json @@ -1,112 +1,124 @@ { - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "rust", - "release-as": "", - "include-component-in-tag": true, - "changelog-sections": [ - { - "type": "feature", - "section": "πŸš€ Features" - }, - { - "type": "feat", - "section": "πŸš€ Features" - }, - { - "type": "fix", - "section": "πŸ› Bug Fixes" - }, - { - "type": "perf", - "section": "⚑ Performance Improvements" - }, - { - "type": "revert", - "section": "◀️ Reverts" - }, - { - "type": "docs", - "section": "πŸ“š Documentation", - "hidden": false - }, - { - "type": "style", - "section": "🎨 Styles", - "hidden": true - }, - { - "type": "chore", - "section": "βš™οΈ Miscellaneous Chores", - "hidden": true - }, - { - "type": "refactor", - "section": "🚜 Code Refactoring", - "hidden": true - }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "rust", + "release-as": "", + "include-component-in-tag": true, + "changelog-sections": [ + { + "type": "feature", + "section": "πŸš€ Features" + }, + { + "type": "feat", + "section": "πŸš€ Features" + }, + { + "type": "fix", + "section": "πŸ› Bug Fixes" + }, + { + "type": "perf", + "section": "⚑ Performance Improvements", + "hidden": false + }, + { + "type": "revert", + "section": "◀️ Reverts" + }, + { + "type": "docs", + "section": "πŸ“š Documentation", + "hidden": false + }, + { + "type": "style", + "section": "🎨 Styles", + "hidden": true + }, + { + "type": "chore", + "section": "βš™οΈ Miscellaneous Chores", + "hidden": true + }, + { + "type": "refactor", + "section": "🚜 Code Refactoring", + "hidden": false + }, + { + "type": "test", + "section": "πŸ§ͺ Tests", + "hidden": true + }, + { + "type": "build", + "section": "πŸ› οΈ Build System", + "hidden": true + }, + { + "type": "ci", + "section": "πŸ₯ Continuous Integration", + "hidden": true + } + ], + "plugins": ["cargo-workspace", "sentence-case"], + "pull-request-header": ":robot: Auto-generated release PR", + "packages": { + "crates/rust-mcp-macros": { + "release-type": "rust", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelogPath": "CHANGELOG.md", + "extra-files": [ { - "type": "test", - "section": "πŸ§ͺ Tests", - "hidden": true - }, + "type": "generic", + "path": "CHANGELOG.md" + } + ] + }, + "crates/rust-mcp-transport": { + "release-type": "rust", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelogPath": "CHANGELOG.md", + "extra-files": [ { - "type": "build", - "section": "πŸ› οΈ Build System", - "hidden": true - }, + "type": "generic", + "path": "CHANGELOG.md" + } + ] + }, + "crates/rust-mcp-extra": { + "release-type": "rust", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelogPath": "CHANGELOG.md", + "extra-files": [ { - "type": "ci", - "section": "πŸ₯ Continuous Integration", - "hidden": true + "type": "generic", + "path": "CHANGELOG.md" } - ], - "plugins": [ - "cargo-workspace", - "sentence-case" - ], - "pull-request-header": ":robot: Auto-generated release PR", - "packages": { - "crates/rust-mcp-macros": { - "release-type": "rust", - "draft": false, - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "changelogPath": "CHANGELOG.md", - "extra-files": [ - { - "type": "generic", - "path": "CHANGELOG.md" - } - ] - }, - "crates/rust-mcp-transport": { - "release-type": "rust", - "draft": false, - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "changelogPath": "CHANGELOG.md", - "extra-files": [ - { - "type": "generic", - "path": "CHANGELOG.md" - } - ] - }, - "crates/rust-mcp-sdk": { - "release-type": "rust", - "draft": false, - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "changelogPath": "CHANGELOG.md", - "extra-files": [ - { - "type": "generic", - "path": "CHANGELOG.md" - } - ] + ] + }, + "crates/rust-mcp-sdk": { + "release-type": "rust", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelogPath": "CHANGELOG.md", + "extra-files": [ + { + "type": "generic", + "path": "CHANGELOG.md" } + ] } -} \ No newline at end of file + } +} diff --git a/.release-manifest.json b/.release-manifest.json index db381e1..c030e49 100644 --- a/.release-manifest.json +++ b/.release-manifest.json @@ -1,15 +1,16 @@ { - "crates/rust-mcp-sdk": "0.7.0", + "crates/rust-mcp-sdk": "0.7.1", "crates/rust-mcp-macros": "0.5.2", - "crates/rust-mcp-transport": "0.6.0", - "examples/hello-world-mcp-server-stdio": "0.1.29", - "examples/hello-world-mcp-server-stdio-core": "0.1.20", - "examples/simple-mcp-client-stdio": "0.1.29", - "examples/simple-mcp-client-stdio-core": "0.1.29", - "examples/hello-world-server-streamable-http-core": "0.1.20", - "examples/hello-world-server-streamable-http": "0.1.32", - "examples/simple-mcp-client-sse-core": "0.1.20", - "examples/simple-mcp-client-sse": "0.1.23", - "examples/simple-mcp-client-streamable-http": "0.1.1", - "examples/simple-mcp-client-streamable-http-core": "0.1.1" + "crates/rust-mcp-transport": "0.6.1", + "crates/rust-mcp-extra": "0.1.1", + "examples/hello-world-mcp-server-stdio": "0.1.30", + "examples/hello-world-mcp-server-stdio-core": "0.1.21", + "examples/simple-mcp-client-stdio": "0.1.30", + "examples/simple-mcp-client-stdio-core": "0.1.30", + "examples/hello-world-server-streamable-http-core": "0.1.21", + "examples/hello-world-server-streamable-http": "0.1.33", + "examples/simple-mcp-client-sse-core": "0.1.21", + "examples/simple-mcp-client-sse": "0.1.24", + "examples/simple-mcp-client-streamable-http": "0.1.2", + "examples/simple-mcp-client-streamable-http-core": "0.1.2" } diff --git a/Cargo.lock b/Cargo.lock index 0acb30d..6fc3e9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" @@ -84,9 +84,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", "zeroize", @@ -94,22 +94,23 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.31.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" +checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", + "libloading", ] [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "bytes", @@ -126,8 +127,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -152,7 +152,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -183,9 +182,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -193,7 +192,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -254,9 +253,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.37" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -379,9 +378,9 @@ checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", ] @@ -441,9 +440,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fnv" @@ -462,9 +461,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd" +checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" dependencies = [ "autocfg", "tokio", @@ -626,9 +625,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" @@ -676,13 +675,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hello-world-mcp-server-stdio" -version = "0.1.29" +version = "0.1.30" dependencies = [ "async-trait", "futures", @@ -696,7 +695,7 @@ dependencies = [ [[package]] name = "hello-world-mcp-server-stdio-core" -version = "0.1.20" +version = "0.1.21" dependencies = [ "async-trait", "futures", @@ -708,7 +707,7 @@ dependencies = [ [[package]] name = "hello-world-server-streamable-http" -version = "0.1.32" +version = "0.1.33" dependencies = [ "async-trait", "futures", @@ -722,7 +721,7 @@ dependencies = [ [[package]] name = "hello-world-server-streamable-http-core" -version = "0.1.20" +version = "0.1.21" dependencies = [ "async-trait", "futures", @@ -911,7 +910,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -1026,9 +1025,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", @@ -1103,9 +1102,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1119,9 +1118,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -1130,9 +1129,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "litemap" version = "0.8.0" @@ -1147,11 +1152,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1184,9 +1188,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1230,6 +1234,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "nom" version = "7.1.3" @@ -1242,11 +1255,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1255,6 +1268,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -1267,9 +1290,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -1288,9 +1311,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1298,15 +1321,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1399,8 +1422,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.0", - "thiserror 2.0.16", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -1421,7 +1444,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -1436,16 +1459,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -1469,6 +1492,17 @@ dependencies = [ "rand_hc", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" @@ -1489,6 +1523,16 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1508,6 +1552,15 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "rand_core" version = "0.9.3" @@ -1517,6 +1570,16 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.2", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1528,18 +1591,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1549,9 +1612,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1560,15 +1623,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -1628,6 +1691,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-mcp-extra" +version = "0.1.1" +dependencies = [ + "base64 0.22.1", + "nanoid", + "once_cell", + "rand 0.9.2", + "rand_distr", + "rust-mcp-sdk", +] + [[package]] name = "rust-mcp-macros" version = "0.5.2" @@ -1642,9 +1717,9 @@ dependencies = [ [[package]] name = "rust-mcp-schema" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb65fd293dbbfabaacba1512b3948cdd9bf31ad1f2c0fed4962052b590c5c44" +checksum = "ba217e6fcb043bba9e194209bff92c35294093187504d1443832ca2051816753" dependencies = [ "serde", "serde_json", @@ -1652,13 +1727,17 @@ dependencies = [ [[package]] name = "rust-mcp-sdk" -version = "0.7.0" +version = "0.7.1" dependencies = [ "async-trait", "axum", "axum-server", "base64 0.22.1", + "bytes", "futures", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", "hyper 1.7.0", "reqwest", "rust-mcp-macros", @@ -1666,7 +1745,7 @@ dependencies = [ "rust-mcp-transport", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -1677,7 +1756,7 @@ dependencies = [ [[package]] name = "rust-mcp-transport" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "bytes", @@ -1686,7 +1765,7 @@ dependencies = [ "rust-mcp-schema", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -1707,9 +1786,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "aws-lc-rs", "once_cell", @@ -1741,9 +1820,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "aws-lc-rs", "ring", @@ -1771,9 +1850,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -1781,18 +1860,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1872,7 +1951,7 @@ dependencies = [ [[package]] name = "simple-mcp-client-sse" -version = "0.1.23" +version = "0.1.24" dependencies = [ "async-trait", "colored", @@ -1880,7 +1959,7 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -1888,7 +1967,7 @@ dependencies = [ [[package]] name = "simple-mcp-client-sse-core" -version = "0.1.20" +version = "0.1.21" dependencies = [ "async-trait", "colored", @@ -1896,7 +1975,7 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -1904,7 +1983,7 @@ dependencies = [ [[package]] name = "simple-mcp-client-stdio" -version = "0.1.29" +version = "0.1.30" dependencies = [ "async-trait", "colored", @@ -1912,13 +1991,13 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", ] [[package]] name = "simple-mcp-client-stdio-core" -version = "0.1.29" +version = "0.1.30" dependencies = [ "async-trait", "colored", @@ -1926,13 +2005,13 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", ] [[package]] name = "simple-mcp-client-streamable-http" -version = "0.1.1" +version = "0.1.2" dependencies = [ "async-trait", "colored", @@ -1940,7 +2019,7 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -1948,7 +2027,7 @@ dependencies = [ [[package]] name = "simple-mcp-client-streamable-http-core" -version = "0.1.1" +version = "0.1.2" dependencies = [ "async-trait", "colored", @@ -1956,7 +2035,7 @@ dependencies = [ "rust-mcp-sdk", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -1986,19 +2065,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" @@ -2048,11 +2127,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -2068,9 +2147,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2088,11 +2167,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", @@ -2156,7 +2236,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.59.0", ] @@ -2174,9 +2254,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2426,9 +2506,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -2439,9 +2519,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -2453,9 +2533,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -2466,9 +2546,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2476,9 +2556,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -2489,9 +2569,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -2511,9 +2591,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -2531,18 +2611,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -2568,7 +2648,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -2589,19 +2678,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2612,9 +2701,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2624,9 +2713,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2636,9 +2725,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -2648,9 +2737,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2660,9 +2749,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2672,9 +2761,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2684,9 +2773,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2696,9 +2785,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wiremock" @@ -2801,9 +2890,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index edb7e28..26fb067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/rust-mcp-macros", "crates/rust-mcp-sdk", "crates/rust-mcp-transport", + "crates/rust-mcp-extra", "examples/simple-mcp-client-stdio", "examples/simple-mcp-client-stdio-core", "examples/hello-world-mcp-server-stdio", @@ -19,9 +20,10 @@ members = [ [workspace.dependencies] # Workspace member crates -rust-mcp-transport = { version = "0.6.0", path = "crates/rust-mcp-transport", default-features = false } +rust-mcp-transport = { version = "0.6.1", path = "crates/rust-mcp-transport", default-features = false } rust-mcp-sdk = { path = "crates/rust-mcp-sdk", default-features = false } rust-mcp-macros = { version = "0.5.2", path = "crates/rust-mcp-macros", default-features = false } +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 } diff --git a/assets/examples/simple-mcp-client-streamable-http.png b/assets/examples/simple-mcp-client-streamable-http.png new file mode 100644 index 0000000..37a80fb Binary files /dev/null and b/assets/examples/simple-mcp-client-streamable-http.png differ diff --git a/crates/rust-mcp-extra/CHANGELOG.md b/crates/rust-mcp-extra/CHANGELOG.md new file mode 100644 index 0000000..7fd2cb3 --- /dev/null +++ b/crates/rust-mcp-extra/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## [0.1.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-extra-v0.1.0...rust-mcp-extra-v0.1.1) (2025-10-13) + + +### πŸš€ Features + +* Initial release v0.1.0 ([4c08beb](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/4c08beb73b102c77e65b724b284008071b7f5ef4)) +* Introduce `rust-mcp-extra` crate for extended id, session, and event store support ([#108](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/108)) ([5fddd3c](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/5fddd3cee12d622c19c23a67d4f381475d914031)) diff --git a/crates/rust-mcp-extra/Cargo.toml b/crates/rust-mcp-extra/Cargo.toml new file mode 100644 index 0000000..982da64 --- /dev/null +++ b/crates/rust-mcp-extra/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rust-mcp-extra" +version = "0.1.1" +authors = ["Ali Hashemi"] +categories = ["api-bindings", "development-tools", "asynchronous", "parsing"] +description = "A companion crate to rust-mcp-sdk offering extra implementations of core traits like SessionStore and EventStore, enabling integration with various database backends and third-party platforms such as AWS Lambda for serverless and cloud-native MCP applications." +repository = "https://github.com/rust-mcp-stack/rust-mcp-sdk" +documentation = "https://docs.rs/rust-mcp-extra" +keywords = ["serverless", "mcp-server", "rust-mcp", "cloud", "lambda"] +license = "MIT" +edition = "2024" +exclude = ["assets/", "tests/"] + +[dependencies] +rust-mcp-sdk = { version = "0.7.1" , path = "../rust-mcp-sdk", default-features = false, features=["server","2025_06_18"] } + +base64 = {workspace = true, optional=true} +nanoid = {version="0.4", optional=true} +once_cell = {version="1.2", optional=true} +rand = {version="0.9.2", features = ["std", "alloc"] , optional=true} +rand_distr = {version="0.5.1", optional=true} + + +[features] +default = ["nano_id","snowflake_id","random_62_id","time_64_id"] +nano_id = ["nanoid"] +snowflake_id = ["once_cell"] +random_62_id = ["rand","rand_distr"] +time_64_id = ["base64"] + +[lints] +workspace = true diff --git a/crates/rust-mcp-extra/README.md b/crates/rust-mcp-extra/README.md new file mode 100644 index 0000000..2adf1d4 --- /dev/null +++ b/crates/rust-mcp-extra/README.md @@ -0,0 +1,54 @@ +# rust-mcp-extra + +**A companion crate to [`rust-mcp-sdk`](https://github.com/rust-mcp-stack/rust-mcp-sdk) providing additional implementations for core traits like `IdGenerator`, `SessionStore` and `EventStore`.** + +----- +## πŸ”’ ID Generators +Various implementations of the IdGenerator trait (from [rust-mcp-sdk]) for generating unique identifiers. + +| **🧩 All ID generators in this crate can be used as `SessionId` generators in [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk)).** + + +| Generator | Description| +| -------------- | ----- | +| **NanoIdGenerator** | Generates short, URL-safe, random string IDs using the [`nanoid`](https://crates.io/crates/nanoid) crate. Ideal for user-friendly, compact identifiers. | +| **TimeBase64Generator** | Encodes the current timestamp (in milliseconds) into a URL-safe Base64 string. Useful when IDs should be time-sortable and compact. | +| **RandomBase62Generator** | Generates alphanumeric [A–Z, a–z, 0–9] strings using random bytes. A simple, reliable option for random unique IDs. | +| **SnowflakeIdGenerator** | Inspired by Twitter’s Snowflake algorithm. Generates 64-bit time-ordered IDs containing timestamp, machine ID, and sequence. Best for distributed or high-throughput systems. | + + +### How to use +Provide an instance of your chosen ID generator in the **HyperServerOptions** when initializing the server. + +For example to use **SnowflakeIdGenerator** : + +```rs +use rust_mcp_extra::id_generator::SnowflakeIdGenerator; + + +let server = hyper_server::create_server( + server_details, + handler, + HyperServerOptions { + host: "127.0.0.1".to_string(), + session_id_generator: Some(Arc::new(SnowflakeIdGenerator::new(1015))), // use SnowflakeIdGenerator + ..Default::default() + }, +); + +``` + +----- + +## πŸ’Ύ Session Stores + +`SessionStore` implementations are available for managing MCP sessions effectively. + +πŸ”œ Coming Soon + +----- + +## πŸ’½ Event Stores +`EventStore` implementations to enable resumability on MCP servers by reliably storing and replaying event histories. + +πŸ”œ Coming Soon diff --git a/crates/rust-mcp-extra/src/http_adaptors.rs b/crates/rust-mcp-extra/src/http_adaptors.rs new file mode 100644 index 0000000..4481404 --- /dev/null +++ b/crates/rust-mcp-extra/src/http_adaptors.rs @@ -0,0 +1 @@ +//! This module provides utility functions for converting between http::Request / http::Response types and framework-specific request/response types. diff --git a/crates/rust-mcp-extra/src/id_generator.rs b/crates/rust-mcp-extra/src/id_generator.rs new file mode 100644 index 0000000..451ddc0 --- /dev/null +++ b/crates/rust-mcp-extra/src/id_generator.rs @@ -0,0 +1,19 @@ +//! This module provides implementations of various ID generators, +//! which can be used for generating `session_id`s in MCP servers. +#[cfg(feature = "nano_id")] +mod nano_id_generator; +#[cfg(feature = "random_62_id")] +mod random_base_62_id_generator; +#[cfg(feature = "snowflake_id")] +mod snow_flake_id_generator; +#[cfg(feature = "time_64_id")] +mod time_base_64_id_generator; + +#[cfg(feature = "nano_id")] +pub use nano_id_generator::*; +#[cfg(feature = "random_62_id")] +pub use random_base_62_id_generator::*; +#[cfg(feature = "snowflake_id")] +pub use snow_flake_id_generator::*; +#[cfg(feature = "time_64_id")] +pub use time_base_64_id_generator::*; diff --git a/crates/rust-mcp-extra/src/id_generator/nano_id_generator.rs b/crates/rust-mcp-extra/src/id_generator/nano_id_generator.rs new file mode 100644 index 0000000..a50ec2b --- /dev/null +++ b/crates/rust-mcp-extra/src/id_generator/nano_id_generator.rs @@ -0,0 +1,70 @@ +//! Short (Smaller than UUID), URL-safe, Customizable alphabet, Cryptographically secure + +use nanoid::nanoid; +use rust_mcp_sdk::id_generator::IdGenerator; + +/// A NanoID-based ID generator that produces short, URL-safe, unique strings. +/// +/// This generator is well-suited for cases where: +/// - You want compact, human-friendly IDs +/// - UUIDs are too long or verbose +/// - You don't need time-based or ordered IDs +/// +/// Internally uses the `nanoid` crate to generate secure, random IDs. +/// +/// # Example +/// ``` +/// use rust_mcp_extra::{id_generator::NanoIdGenerator,IdGenerator}; +/// +/// let generator = NanoIdGenerator::new(10); +/// let id: String = generator.generate(); +/// println!("Generated ID: {}", id); +/// assert_eq!(id.len(), 10); +/// ``` +pub struct NanoIdGenerator { + size: usize, // number of characters in the ID +} + +impl NanoIdGenerator { + /// Creates a new Nano ID generator. + /// + /// # Arguments + /// * `size` - Length of the generated ID (default: 21 if unsure) + pub fn new(size: usize) -> Self { + Self { size } + } +} + +impl IdGenerator for NanoIdGenerator +where + T: From, +{ + fn generate(&self) -> T { + let size = self.size; + let id = nanoid!(size); + T::from(id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_correct_length_id() { + let generator = NanoIdGenerator::new(12); + let id: String = generator.generate(); + assert_eq!(id.len(), 12); + } + + #[test] + fn generates_unique_ids() { + let generator = NanoIdGenerator::new(8); + let mut seen = std::collections::HashSet::new(); + + for _ in 0..1000 { + let id: String = generator.generate(); + assert!(seen.insert(id.clone()), "Duplicate ID: {}", id); + } + } +} diff --git a/crates/rust-mcp-extra/src/id_generator/random_base_62_id_generator.rs b/crates/rust-mcp-extra/src/id_generator/random_base_62_id_generator.rs new file mode 100644 index 0000000..1fcbe00 --- /dev/null +++ b/crates/rust-mcp-extra/src/id_generator/random_base_62_id_generator.rs @@ -0,0 +1,87 @@ +//! Short, URL-safe, No collisions if length is sufficient +//! Needs collision handling if critical + +use rand::Rng; +use rand_distr::Alphanumeric; +use rust_mcp_sdk::id_generator::IdGenerator; + +/// A random Base62 ID generator. +/// +/// Generates short, random alphanumeric strings composed of [A-Z, a-z, 0-9]. +/// Useful when you want compact, URL-safe random IDs without needing +/// time-based ordering. +/// +/// # Example +/// ``` +/// use rust_mcp_extra::{id_generator::RandomBase62Generator,IdGenerator}; +/// +/// let generator = RandomBase62Generator::new(12); +/// let id: String = generator.generate(); +/// println!("Generated Base62 ID: {}", id); +/// ``` +pub struct RandomBase62Generator { + size: usize, +} + +impl RandomBase62Generator { + /// Creates a new random Base62 ID generator. + /// + /// # Arguments + /// * `size` - Length of the generated ID. + pub fn new(size: usize) -> Self { + Self { size } + } +} + +impl IdGenerator for RandomBase62Generator +where + T: From, +{ + /// Generates a new random Base62 ID string. + /// + /// The ID consists of randomly selected alphanumeric characters (A-Z, a-z, 0-9). + fn generate(&self) -> T { + let id: String = rand::rng() + .sample_iter(&Alphanumeric) + .take(self.size) + .map(char::from) + .collect(); + + T::from(id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_non_empty_id() { + let generator = RandomBase62Generator::new(16); + let id: String = generator.generate(); + assert_eq!(id.len(), 16); + assert!(!id.is_empty()); + } + + #[test] + fn generates_unique_ids() { + let generator = RandomBase62Generator::new(8); + let mut seen = std::collections::HashSet::new(); + + for _ in 0..1000 { + let id: String = generator.generate(); + assert!(seen.insert(id), "Duplicate ID generated"); + } + } + + #[test] + fn only_alphanumeric_characters() { + let generator = RandomBase62Generator::new(50); + let id: String = generator.generate(); + + assert!( + id.chars().all(|c| c.is_ascii_alphanumeric()), + "ID contains non-alphanumeric chars" + ); + } +} diff --git a/crates/rust-mcp-extra/src/id_generator/snow_flake_id_generator.rs b/crates/rust-mcp-extra/src/id_generator/snow_flake_id_generator.rs new file mode 100644 index 0000000..39942ec --- /dev/null +++ b/crates/rust-mcp-extra/src/id_generator/snow_flake_id_generator.rs @@ -0,0 +1,166 @@ +//! Medium size ,Globally unique , Time-sortable , Compact (64 bits), +//! Use case: Distributed systems needing high-throughput, unique IDs without collisions. +//! [ timestamp (41 bits) | machine id (10 bits) | sequence (12 bits) ] + +use once_cell::sync::Lazy; +use rust_mcp_sdk::id_generator::IdGenerator; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Epoch (customizable to reduce total bits needed) +static SHORTER_EPOCH: Lazy = Lazy::new(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("invalid system time!") + .as_millis() as u64 +}); + +/// A Snowflake ID generator implementation producing 64-bit unique IDs. +/// +/// Snowflake IDs are composed of: +/// - A timestamp in milliseconds since a custom epoch (usually a fixed past time), +/// - A machine ID (or worker ID) to differentiate between nodes, +/// - A sequence number that increments within the same millisecond to avoid collisions. +/// +/// Format (64 bits total): +/// - 41 bits: timestamp (ms since SHORTER_EPOCH) +/// - 10 bits: machine ID (0-1023) +/// - 12 bits: sequence number (per ms) +/// +/// This generator ensures: +/// - Uniqueness across multiple machines (given unique machine IDs), +/// - Monotonic increasing IDs when generated in the same process, +/// - Thread safety with internal locking. +pub struct SnowflakeIdGenerator { + machine_id: u16, // 10 bits max + last_timestamp: AtomicU64, + sequence: AtomicU64, +} + +impl SnowflakeIdGenerator { + pub fn new(machine_id: u16) -> Self { + assert!( + machine_id < 1024, + "Machine ID must be less than 1024 (10 bits)" + ); + SnowflakeIdGenerator { + machine_id, + last_timestamp: AtomicU64::new(0), + sequence: AtomicU64::new(0), + } + } + + fn current_timestamp(&self) -> u64 { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("invalid system time!") + .as_millis() as u64; + + now - *SHORTER_EPOCH + } + + fn next_id(&self) -> u64 { + let mut timestamp = self.current_timestamp(); + + let last_ts = self.last_timestamp.load(Ordering::Relaxed); + + let sequence = if timestamp == last_ts { + // same millisecond β€” increment sequence + let seq = self.sequence.fetch_add(1, Ordering::Relaxed) & 0xFFF; // 12 bits + if seq == 0 { + // Sequence overflow β€” wait for next ms + while timestamp <= last_ts { + timestamp = self.current_timestamp(); + } + self.sequence.store(0, Ordering::Relaxed); + self.last_timestamp.store(timestamp, Ordering::Relaxed); + 0 + } else { + seq + } + } else { + // new timestamp + self.sequence.store(0, Ordering::Relaxed); + self.last_timestamp.store(timestamp, Ordering::Relaxed); + 0 + }; + + // Compose ID: [timestamp][machine_id][sequence] + ((timestamp & 0x1FFFFFFFFFF) << 22) // 41 bits + | ((self.machine_id as u64 & 0x3FF) << 12) // 10 bits + | (sequence & 0xFFF) // 12 bits + } +} + +impl IdGenerator for SnowflakeIdGenerator +where + T: From, +{ + fn generate(&self) -> T { + let id = self.next_id(); + T::from(id.to_string()) // We could optionally encode it to base64 or base62 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_id() { + let generator = SnowflakeIdGenerator::new(1); + let id: String = generator.generate(); + assert!(!id.is_empty(), "Generated ID should not be empty"); + } + + #[test] + fn generates_unique_ids() { + let generator = SnowflakeIdGenerator::new(1); + let mut ids = std::collections::HashSet::new(); + for _ in 0..1000 { + let id: String = generator.generate(); + assert!(ids.insert(id), "Duplicate ID generated"); + } + } + + #[test] + fn ids_are_monotonic_increasing() { + let generator = SnowflakeIdGenerator::new(1); + let mut prev_id = 0u64; + + for _ in 0..1000 { + let id: String = generator.generate(); + let current_id: u64 = id.parse().expect("ID should be a valid u64"); + assert!( + current_id > prev_id, + "ID not strictly increasing: {} <= {}", + current_id, + prev_id + ); + prev_id = current_id; + } + } + + #[test] + fn handles_sequence_rollover() { + // Try to simulate a sequence rollover by generating many IDs quickly + // just ensuring it doesn't panic + let generator = SnowflakeIdGenerator::new(1); + for _ in 0..2000 { + let _id: String = generator.generate(); + } + } + + #[test] + fn respects_machine_id_limit() { + // Valid machine ID + let _ = SnowflakeIdGenerator::new(1023); + } + + #[test] + #[should_panic(expected = "Machine ID must be less than 1024")] + fn rejects_invalid_machine_id() { + // Invalid machine ID (greater than 1023) + let _ = SnowflakeIdGenerator::new(1024); + } +} diff --git a/crates/rust-mcp-extra/src/id_generator/time_base_64_id_generator.rs b/crates/rust-mcp-extra/src/id_generator/time_base_64_id_generator.rs new file mode 100644 index 0000000..a04fede --- /dev/null +++ b/crates/rust-mcp-extra/src/id_generator/time_base_64_id_generator.rs @@ -0,0 +1,111 @@ +//! Short, Fast, Sortable, Shorter than UUID +//! Not globally unique + +use base64::Engine; +use base64::engine::general_purpose; +use rust_mcp_sdk::id_generator::IdGenerator; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// A time-based ID generator that produces Base64-encoded timestamps. +/// +/// This generator encodes the current timestamp in milliseconds since UNIX epoch +/// as a URL-safe Base64 string without padding. Optionally, it can prefix the ID +/// with a static string for better readability or namespacing. +/// +/// # Example +/// ``` +/// use rust_mcp_extra::{id_generator::TimeBase64Generator, IdGenerator}; +/// +/// let generator = TimeBase64Generator::new(Some("ts_")); +/// let id: String = generator.generate(); +/// println!("Generated time-based ID: {}", id); +/// ``` +pub struct TimeBase64Generator { + prefix: &'static str, +} + +impl TimeBase64Generator { + /// Creates a new time-based Base64 ID generator with an optional prefix. + /// + /// # Arguments + /// * `prefix` - Optional static string to prepend to generated IDs. + pub fn new(prefix: Option<&'static str>) -> Self { + Self { + prefix: prefix.unwrap_or(""), + } + } + + /// Returns current timestamp in milliseconds since UNIX epoch. + fn current_millis() -> u64 { + let now = SystemTime::now(); + let duration = now + .duration_since(UNIX_EPOCH) + .expect("invalid system time!"); + duration.as_millis() as u64 + } +} + +impl IdGenerator for TimeBase64Generator +where + T: From, +{ + /// Generates a new time-based Base64 ID. + /// + /// The ID is the current timestamp encoded as a URL-safe Base64 string (no padding), + /// optionally prefixed by the configured prefix. + fn generate(&self) -> T { + let timestamp = Self::current_millis(); + let bytes = timestamp.to_le_bytes(); + let encoded = general_purpose::URL_SAFE_NO_PAD.encode(bytes); + + if self.prefix.is_empty() { + T::from(encoded) + } else { + T::from(format!("{}{}", self.prefix, encoded)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_non_empty_id() { + let generator = TimeBase64Generator::new(None); + let id: String = generator.generate(); + assert!(!id.is_empty(), "ID should not be empty"); + } + + #[test] + fn generates_id_with_prefix() { + let prefix = "ts_"; + let generator = TimeBase64Generator::new(Some(prefix)); + let id: String = generator.generate(); + assert!(id.starts_with(prefix), "ID should start with prefix"); + } + + #[test] + fn ids_change_over_time() { + let generator = TimeBase64Generator::new(None); + let id1: String = generator.generate(); + std::thread::sleep(std::time::Duration::from_millis(2)); + let id2: String = generator.generate(); + assert_ne!(id1, id2, "IDs generated at different times should differ"); + } + + #[test] + fn base64_decodes_to_timestamp() { + let generator = TimeBase64Generator::new(None); + let id: String = generator.generate(); + + // Decode the base64 (without prefix) + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(&id) + .expect("Should decode base64"); + + // Convert bytes back to u64 timestamp + let timestamp = u64::from_le_bytes(decoded.try_into().unwrap()); + assert!(timestamp > 0, "Timestamp should be positive"); + } +} diff --git a/crates/rust-mcp-extra/src/lib.rs b/crates/rust-mcp-extra/src/lib.rs new file mode 100644 index 0000000..f4a3d0e --- /dev/null +++ b/crates/rust-mcp-extra/src/lib.rs @@ -0,0 +1,5 @@ +pub mod http_adaptors; +pub mod id_generator; +pub mod sqlite; + +pub use rust_mcp_sdk::id_generator::IdGenerator; diff --git a/crates/rust-mcp-extra/src/sqlite.rs b/crates/rust-mcp-extra/src/sqlite.rs new file mode 100644 index 0000000..12e737c --- /dev/null +++ b/crates/rust-mcp-extra/src/sqlite.rs @@ -0,0 +1,7 @@ +mod sqlite_event_store; +mod sqlite_session_store; + +#[allow(unused)] +pub use sqlite_event_store::*; +#[allow(unused)] +pub use sqlite_session_store::*; diff --git a/crates/rust-mcp-extra/src/sqlite/sqlite_event_store.rs b/crates/rust-mcp-extra/src/sqlite/sqlite_event_store.rs new file mode 100644 index 0000000..1bda222 --- /dev/null +++ b/crates/rust-mcp-extra/src/sqlite/sqlite_event_store.rs @@ -0,0 +1 @@ +//! This module serves as a placeholder for implementing a SQLite-backed event store. diff --git a/crates/rust-mcp-extra/src/sqlite/sqlite_session_store.rs b/crates/rust-mcp-extra/src/sqlite/sqlite_session_store.rs new file mode 100644 index 0000000..a6f97a3 --- /dev/null +++ b/crates/rust-mcp-extra/src/sqlite/sqlite_session_store.rs @@ -0,0 +1 @@ +//! This module serves as a placeholder for implementing a SQLite-backed session store. diff --git a/crates/rust-mcp-sdk/CHANGELOG.md b/crates/rust-mcp-sdk/CHANGELOG.md index 4fde908..7d56e22 100644 --- a/crates/rust-mcp-sdk/CHANGELOG.md +++ b/crates/rust-mcp-sdk/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.7.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.7.0...rust-mcp-sdk-v0.7.1) (2025-10-13) + + +### πŸš€ Features + +* Add server_supports_completion method ([#104](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/104)) ([6268726](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/62687262a30cce0928435c153b6016d56e85b8ee)) +* **server:** Decouple core logic from HTTP server for improved architecture ([#106](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/106)) ([d10488b](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/d10488bac739bf28b45d636129eb598d4dd87fd2)) + + +### ⚑ Performance Improvements + +* Remove unnecessary mutex in the session store ([ea5d580](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/ea5d58013ac051f2bbe7e9f5b3a20a3220e66c9b)) + + +### 🚜 Code Refactoring + +* Expose Store Traits and add ToMcpServerHandler for Improved Framework Flexibility ([#107](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/107)) ([5bf54d6](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/5bf54d6d442d6cb854242697fa50c29bca0b8483)) + ## [0.7.0](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-sdk-v0.6.3...rust-mcp-sdk-v0.7.0) (2025-09-19) diff --git a/crates/rust-mcp-sdk/Cargo.toml b/crates/rust-mcp-sdk/Cargo.toml index 8bba7c7..0ecc527 100644 --- a/crates/rust-mcp-sdk/Cargo.toml +++ b/crates/rust-mcp-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-mcp-sdk" -version = "0.7.0" +version = "0.7.1" authors = ["Ali Hashemi"] categories = ["data-structures", "parser-implementations", "parsing"] description = "An asynchronous SDK and framework for building MCP-Servers and MCP-Clients, leveraging the rust-mcp-schema for type safe MCP Schema Objects." @@ -29,9 +29,13 @@ tokio-stream = { workspace = true, optional = true } axum-server = { version = "0.7", features = [], optional = true } tracing.workspace = true base64.workspace = true +bytes.workspace = true # rustls = { workspace = true, optional = true } hyper = { version = "1.6.0", optional = true } +http = { version ="1.3", optional = true } +http-body-util = { version ="0.1", optional = true } +http-body = { version ="1.0", optional = true } [dev-dependencies] wiremock = "0.5" @@ -61,13 +65,13 @@ default = [ "2025_06_18", ] # All features enabled by default -sse = ["rust-mcp-transport/sse"] -streamable-http = ["rust-mcp-transport/streamable-http"] +sse = ["rust-mcp-transport/sse","http","http-body","http-body-util"] +streamable-http = ["rust-mcp-transport/streamable-http","http","http-body","http-body-util"] stdio = ["rust-mcp-transport/stdio"] server = [] # Server feature client = [] # Client feature -hyper-server = ["axum", "axum-server", "hyper", "server", "tokio-stream"] +hyper-server = ["axum", "axum-server", "hyper", "server", "tokio-stream","http","http-body","http-body-util"] ssl = ["axum-server/tls-rustls"] tls-no-provider = ["axum-server/tls-rustls-no-provider"] macros = ["rust-mcp-macros/sdk"] diff --git a/crates/rust-mcp-sdk/src/hyper_servers.rs b/crates/rust-mcp-sdk/src/hyper_servers.rs index f18c428..87307c0 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers.rs @@ -1,12 +1,8 @@ -mod app_state; pub mod error; pub mod hyper_runtime; pub mod hyper_server; pub mod hyper_server_core; -mod middlewares; mod routes; mod server; -mod session_store; pub use server::*; -pub use session_store::*; diff --git a/crates/rust-mcp-sdk/src/hyper_servers/error.rs b/crates/rust-mcp-sdk/src/hyper_servers/error.rs index 74cbcd1..dd55d8f 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/error.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/error.rs @@ -15,6 +15,8 @@ pub enum TransportServerError { StreamIoError(String), #[error("{0}")] AddrParseError(#[from] AddrParseError), + #[error("{0}")] + HttpError(String), #[error("Server start error: {0}")] ServerStartError(String), #[error("Invalid options: {0}")] 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 85cf791..5cedb59 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_runtime.rs @@ -1,6 +1,7 @@ use std::{sync::Arc, time::Duration}; use crate::{ + mcp_http::McpAppState, mcp_server::HyperServer, schema::{ schema_utils::{NotificationFromServer, RequestFromServer, ResultFromClient}, @@ -14,11 +15,10 @@ use crate::{ use axum_server::Handle; use rust_mcp_transport::SessionId; -use tokio::{sync::Mutex, task::JoinHandle}; +use tokio::task::JoinHandle; use crate::{ error::SdkResult, - hyper_servers::app_state::AppState, mcp_server::{ error::{TransportServerError, TransportServerResult}, ServerRuntime, @@ -26,7 +26,7 @@ use crate::{ }; pub struct HyperRuntime { - pub(crate) state: Arc, + pub(crate) state: Arc, pub(crate) server_task: JoinHandle>, pub(crate) server_handle: Handle, } @@ -79,7 +79,7 @@ impl HyperRuntime { pub async fn runtime_by_session( &self, session_id: &SessionId, - ) -> TransportServerResult>>> { + ) -> TransportServerResult> { self.state.session_store.get(session_id).await.ok_or( TransportServerError::SessionIdInvalid(session_id.to_string()), ) @@ -92,7 +92,6 @@ impl HyperRuntime { timeout: Option, ) -> SdkResult { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.request(request, timeout).await } @@ -102,7 +101,6 @@ impl HyperRuntime { notification: NotificationFromServer, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_notification(notification).await } @@ -117,7 +115,6 @@ impl HyperRuntime { params: Option, ) -> SdkResult { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.list_roots(params).await } @@ -127,7 +124,6 @@ impl HyperRuntime { params: LoggingMessageNotificationParams, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_logging_message(params).await } @@ -140,7 +136,6 @@ impl HyperRuntime { params: Option, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_prompt_list_changed(params).await } @@ -153,7 +148,6 @@ impl HyperRuntime { params: Option, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_resource_list_changed(params).await } @@ -166,7 +160,6 @@ impl HyperRuntime { params: ResourceUpdatedNotificationParams, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_resource_updated(params).await } @@ -179,7 +172,6 @@ impl HyperRuntime { params: Option, ) -> SdkResult<()> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.send_tool_list_changed(params).await } @@ -199,7 +191,6 @@ impl HyperRuntime { timeout: Option, ) -> SdkResult { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.ping(timeout).await } @@ -214,7 +205,6 @@ impl HyperRuntime { params: CreateMessageRequestParams, ) -> SdkResult { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); runtime.create_message(params).await } @@ -223,7 +213,6 @@ impl HyperRuntime { session_id: &SessionId, ) -> SdkResult> { let runtime = self.runtime_by_session(session_id).await?; - let runtime = runtime.lock().await.to_owned(); Ok(runtime.client_info()) } } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/middlewares.rs b/crates/rust-mcp-sdk/src/hyper_servers/middlewares.rs deleted file mode 100644 index 0222952..0000000 --- a/crates/rust-mcp-sdk/src/hyper_servers/middlewares.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod protect_dns_rebinding; -pub(crate) mod session_id_gen; diff --git a/crates/rust-mcp-sdk/src/hyper_servers/middlewares/protect_dns_rebinding.rs b/crates/rust-mcp-sdk/src/hyper_servers/middlewares/protect_dns_rebinding.rs deleted file mode 100644 index 5674e87..0000000 --- a/crates/rust-mcp-sdk/src/hyper_servers/middlewares/protect_dns_rebinding.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::hyper_servers::app_state::AppState; -use crate::schema::schema_utils::SdkError; -use axum::{ - extract::{Request, State}, - middleware::Next, - response::IntoResponse, - Json, -}; -use hyper::{ - header::{HOST, ORIGIN}, - HeaderMap, StatusCode, -}; -use std::sync::Arc; - -// Middleware to protect against DNS rebinding attacks by validating Host and Origin headers. -pub async fn protect_dns_rebinding( - headers: HeaderMap, - State(state): State>, - request: Request, - next: Next, -) -> impl IntoResponse { - if !state.needs_dns_protection() { - // If protection is not needed, pass the request to the next handler - return next.run(request).await.into_response(); - } - - if let Some(allowed_hosts) = state.allowed_hosts.as_ref() { - if !allowed_hosts.is_empty() { - let Some(host) = headers.get(HOST).and_then(|h| h.to_str().ok()) else { - let error = SdkError::bad_request().with_message("Invalid Host header: [unknown] "); - return (StatusCode::FORBIDDEN, Json(error)).into_response(); - }; - - if !allowed_hosts - .iter() - .any(|allowed| allowed.eq_ignore_ascii_case(host)) - { - let error = SdkError::bad_request() - .with_message(format!("Invalid Host header: \"{host}\" ").as_str()); - return (StatusCode::FORBIDDEN, Json(error)).into_response(); - } - } - } - - if let Some(allowed_origins) = state.allowed_origins.as_ref() { - if !allowed_origins.is_empty() { - let Some(origin) = headers.get(ORIGIN).and_then(|h| h.to_str().ok()) else { - let error = - SdkError::bad_request().with_message("Invalid Origin header: [unknown] "); - return (StatusCode::FORBIDDEN, Json(error)).into_response(); - }; - - if !allowed_origins - .iter() - .any(|allowed| allowed.eq_ignore_ascii_case(origin)) - { - let error = SdkError::bad_request() - .with_message(format!("Invalid Origin header: \"{origin}\" ").as_str()); - return (StatusCode::FORBIDDEN, Json(error)).into_response(); - } - } - } - - // If all checks pass, proceed to the next handler in the chain - next.run(request).await -} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/middlewares/session_id_gen.rs b/crates/rust-mcp-sdk/src/hyper_servers/middlewares/session_id_gen.rs deleted file mode 100644 index b68b325..0000000 --- a/crates/rust-mcp-sdk/src/hyper_servers/middlewares/session_id_gen.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{Request, State}, - middleware::Next, - response::Response, -}; -use hyper::StatusCode; -use rust_mcp_transport::SessionId; - -use crate::hyper_servers::app_state::AppState; - -// Middleware to generate and attach a session ID -pub async fn generate_session_id( - State(state): State>, - mut request: Request, - next: Next, -) -> Result { - let session_id: SessionId = state.id_generator.generate(); - request.extensions_mut().insert(session_id); - // Proceed to the next middleware or handler - Ok(next.run(request).await) -} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes.rs index b1b15fc..4ae274b 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/routes.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes.rs @@ -1,10 +1,12 @@ pub mod fallback_routes; -mod hyper_utils; pub mod messages_routes; +#[cfg(feature = "sse")] pub mod sse_routes; pub mod streamable_http_routes; -use super::{app_state::AppState, HyperServerOptions}; +use crate::mcp_http::McpAppState; + +use super::HyperServerOptions; use axum::Router; use std::sync::Arc; @@ -19,21 +21,23 @@ use std::sync::Arc; /// /// # Returns /// * `Router` - An Axum router configured with all application routes and state -pub fn app_routes(state: Arc, server_options: &HyperServerOptions) -> Router { +pub fn app_routes(state: Arc, server_options: &HyperServerOptions) -> Router { let router: Router = Router::new() .merge(streamable_http_routes::routes( - state.clone(), server_options.streamable_http_endpoint(), )) .merge({ let mut r = Router::new(); + #[cfg(feature = "sse")] if server_options.sse_support { r = r .merge(sse_routes::routes( - state.clone(), server_options.sse_endpoint(), + server_options.sse_messages_endpoint(), + )) + .merge(messages_routes::routes( + server_options.sse_messages_endpoint(), )) - .merge(messages_routes::routes(state.clone())) } r }) diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes/hyper_utils.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes/hyper_utils.rs deleted file mode 100644 index 7101a73..0000000 --- a/crates/rust-mcp-sdk/src/hyper_servers/routes/hyper_utils.rs +++ /dev/null @@ -1,502 +0,0 @@ -use crate::{ - error::SdkResult, - hyper_servers::{ - app_state::AppState, - error::{TransportServerError, TransportServerResult}, - }, - mcp_runtimes::server_runtime::DEFAULT_STREAM_ID, - mcp_server::{server_runtime, ServerRuntime}, - mcp_traits::{mcp_handler::McpServerHandler, IdGenerator}, - utils::validate_mcp_protocol_version, -}; - -use crate::schema::schema_utils::{ClientMessage, SdkError}; - -use axum::{http::HeaderValue, response::IntoResponse}; -use axum::{ - response::{ - sse::{Event, KeepAlive}, - Sse, - }, - Json, -}; -use futures::stream; -use hyper::{header, HeaderMap, StatusCode}; -use rust_mcp_transport::{ - EventId, McpDispatch, SessionId, SseTransport, StreamId, ID_SEPARATOR, - MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_ID_HEADER, -}; -use std::{sync::Arc, time::Duration}; -use tokio::io::{duplex, AsyncBufReadExt, BufReader}; - -const DUPLEX_BUFFER_SIZE: usize = 8192; - -async fn create_sse_stream( - runtime: Arc, - session_id: SessionId, - state: Arc, - payload: Option<&str>, - standalone: bool, - last_event_id: Option, -) -> TransportServerResult> { - let payload_string = payload.map(|p| p.to_string()); - - // TODO: this logic should be moved out after refactoing the mcp_stream.rs - let payload_contains_request = payload_string - .as_ref() - .map(|json_str| contains_request(json_str)) - .unwrap_or(Ok(false)); - let Ok(payload_contains_request) = payload_contains_request else { - return Ok((StatusCode::BAD_REQUEST, Json(SdkError::parse_error())).into_response()); - }; - - // readable stream of string to be used in transport - let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); - // writable stream to deliver message to the client - let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); - - let session_id = Arc::new(session_id); - let stream_id: Arc = if standalone { - Arc::new(DEFAULT_STREAM_ID.to_string()) - } else { - Arc::new(state.stream_id_gen.generate()) - }; - - let event_store = state.event_store.as_ref().map(Arc::clone); - let resumability_enabled = event_store.is_some(); - - let mut transport = SseTransport::::new( - read_rx, - write_tx, - read_tx, - Arc::clone(&state.transport_options), - ) - .map_err(|err| TransportServerError::TransportError(err.to_string()))?; - if let Some(event_store) = event_store.clone() { - transport.make_resumable((*session_id).clone(), (*stream_id).clone(), event_store); - } - let transport = Arc::new(transport); - - let ping_interval = state.ping_interval; - let runtime_clone = Arc::clone(&runtime); - let stream_id_clone = stream_id.clone(); - let transport_clone = transport.clone(); - - //Start the server runtime - tokio::spawn(async move { - match runtime_clone - .start_stream( - transport_clone, - &stream_id_clone, - ping_interval, - payload_string, - ) - .await - { - Ok(_) => tracing::trace!("stream {} exited gracefully.", &stream_id_clone), - Err(err) => tracing::info!("stream {} exited with error : {}", &stream_id_clone, err), - } - let _ = runtime.remove_transport(&stream_id_clone).await; - }); - - // Construct SSE stream - let reader = BufReader::new(write_rx); - - // send outgoing messages from server to the client over the sse stream - let message_stream = stream::unfold(reader, move |mut reader| { - async move { - let mut line = String::new(); - - match reader.read_line(&mut line).await { - Ok(0) => None, // EOF - Ok(_) => { - let trimmed_line = line.trim_end_matches('\n').to_owned(); - - // empty sse comment to keep-alive - if is_empty_sse_message(&trimmed_line) { - return Some((Ok(Event::default()), reader)); - } - - let (event_id, message) = match ( - resumability_enabled, - trimmed_line.split_once(char::from(ID_SEPARATOR)), - ) { - (true, Some((id, msg))) => (Some(id.to_string()), msg.to_string()), - _ => (None, trimmed_line), - }; - - let event = match event_id { - Some(id) => Event::default().data(message).id(id), - None => Event::default().data(message), - }; - - Some((Ok(event), reader)) - } - Err(e) => Some((Err(e), reader)), - } - } - }); - - let sse_stream = - Sse::new(message_stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))); - - // Return SSE response with keep-alive - // Create a Response and set headers - let mut response = sse_stream.into_response(); - response.headers_mut().insert( - MCP_SESSION_ID_HEADER, - HeaderValue::from_str(&session_id).unwrap(), - ); - - // if last_event_id exists we replay messages from the event-store - tokio::spawn(async move { - if let Some(last_event_id) = last_event_id { - if let Some(event_store) = state.event_store.as_ref() { - if let Some(events) = event_store.events_after(last_event_id).await { - for message_payload in events.messages { - // skip storing replay messages - let error = transport.write_str(&message_payload, true).await; - if let Err(error) = error { - tracing::trace!("Error replaying message: {error}") - } - } - } - } - } - }); - - if !payload_contains_request { - *response.status_mut() = StatusCode::ACCEPTED; - } - Ok(response) -} - -// TODO: this function will be removed after refactoring the readable stream of the transports -// so we would deserialize the string syncronousely and have more control over the flow -// this function may incur a slight runtime cost which could be avoided after refactoring -fn contains_request(json_str: &str) -> Result { - let value: serde_json::Value = serde_json::from_str(json_str)?; - match value { - serde_json::Value::Object(obj) => Ok(obj.contains_key("id") && obj.contains_key("method")), - serde_json::Value::Array(arr) => Ok(arr.iter().any(|item| { - item.as_object() - .map(|obj| obj.contains_key("id") && obj.contains_key("method")) - .unwrap_or(false) - })), - _ => Ok(false), - } -} - -fn is_result(json_str: &str) -> Result { - let value: serde_json::Value = serde_json::from_str(json_str)?; - match value { - serde_json::Value::Object(obj) => Ok(obj.contains_key("result")), - serde_json::Value::Array(arr) => Ok(arr.iter().all(|item| { - item.as_object() - .map(|obj| obj.contains_key("result")) - .unwrap_or(false) - })), - _ => Ok(false), - } -} - -pub async fn create_standalone_stream( - session_id: SessionId, - last_event_id: Option, - state: Arc, -) -> TransportServerResult> { - let runtime = state.session_store.get(&session_id).await.ok_or( - TransportServerError::SessionIdInvalid(session_id.to_string()), - )?; - let runtime = runtime.lock().await.to_owned(); - - if runtime.stream_id_exists(DEFAULT_STREAM_ID).await { - let error = - SdkError::bad_request().with_message("Only one SSE stream is allowed per session"); - return Ok((StatusCode::CONFLICT, Json(error)).into_response()); - } - - if let Some(last_event_id) = last_event_id.as_ref() { - tracing::trace!( - "SSE stream re-connected with last-event-id: {}", - last_event_id - ); - } - - let mut response = create_sse_stream( - runtime.clone(), - session_id.clone(), - state.clone(), - None, - true, - last_event_id, - ) - .await?; - *response.status_mut() = StatusCode::OK; - Ok(response) -} - -pub async fn start_new_session( - state: Arc, - payload: &str, -) -> TransportServerResult> { - let session_id: SessionId = state.id_generator.generate(); - - let h: Arc = state.handler.clone(); - // create a new server instance with unique session_id and - let runtime: Arc = server_runtime::create_server_instance( - Arc::clone(&state.server_details), - h, - session_id.to_owned(), - ); - - tracing::info!("a new client joined : {}", &session_id); - - let response = create_sse_stream( - runtime.clone(), - session_id.clone(), - state.clone(), - Some(payload), - false, - None, - ) - .await; - - if response.is_ok() { - state - .session_store - .set(session_id.to_owned(), runtime.clone()) - .await; - } - response -} - -async fn single_shot_stream( - runtime: Arc, - session_id: SessionId, - state: Arc, - payload: Option<&str>, - standalone: bool, -) -> TransportServerResult> { - // readable stream of string to be used in transport - let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); - // writable stream to deliver message to the client - let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); - - let transport = SseTransport::::new( - read_rx, - write_tx, - read_tx, - Arc::clone(&state.transport_options), - ) - .map_err(|err| TransportServerError::TransportError(err.to_string()))?; - - let stream_id = if standalone { - DEFAULT_STREAM_ID.to_string() - } else { - state.id_generator.generate() - }; - let ping_interval = state.ping_interval; - let runtime_clone = Arc::clone(&runtime); - - let payload_string = payload.map(|p| p.to_string()); - - tokio::spawn(async move { - match runtime_clone - .start_stream( - Arc::new(transport), - &stream_id, - ping_interval, - payload_string, - ) - .await - { - Ok(_) => tracing::info!("stream {} exited gracefully.", &stream_id), - Err(err) => tracing::info!("stream {} exited with error : {}", &stream_id, err), - } - let _ = runtime.remove_transport(&stream_id).await; - }); - - let mut reader = BufReader::new(write_rx); - let mut line = String::new(); - let response = match reader.read_line(&mut line).await { - Ok(0) => None, // EOF - Ok(_) => { - let trimmed_line = line.trim_end_matches('\n').to_owned(); - Some(Ok(trimmed_line)) - } - Err(e) => Some(Err(e)), - }; - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ); - headers.insert( - MCP_SESSION_ID_HEADER, - HeaderValue::from_str(&session_id).unwrap(), - ); - - match response { - Some(response_result) => match response_result { - Ok(response_str) => { - Ok((StatusCode::OK, headers, response_str.to_string()).into_response()) - } - Err(err) => Ok(( - StatusCode::INTERNAL_SERVER_ERROR, - headers, - Json(err.to_string()), - ) - .into_response()), - }, - None => Ok(( - StatusCode::UNPROCESSABLE_ENTITY, - headers, - Json("End of the transport stream reached."), - ) - .into_response()), - } -} - -pub async fn process_incoming_message_return( - session_id: SessionId, - state: Arc, - payload: &str, -) -> TransportServerResult { - match state.session_store.get(&session_id).await { - Some(runtime) => { - let runtime = runtime.lock().await.to_owned(); - - single_shot_stream( - runtime.clone(), - session_id.clone(), - state.clone(), - Some(payload), - false, - ) - .await - // Ok(StatusCode::OK.into_response()) - } - None => { - let error = SdkError::session_not_found(); - Ok((StatusCode::NOT_FOUND, Json(error)).into_response()) - } - } -} - -pub async fn process_incoming_message( - session_id: SessionId, - state: Arc, - payload: &str, -) -> TransportServerResult { - match state.session_store.get(&session_id).await { - Some(runtime) => { - let runtime = runtime.lock().await.to_owned(); - // when receiving a result in a streamable_http server, that means it was sent by the standalone sse transport - // it should be processed by the same transport , therefore no need to call create_sse_stream - let Ok(is_result) = is_result(payload) else { - return Ok((StatusCode::BAD_REQUEST, Json(SdkError::parse_error())).into_response()); - }; - - if is_result { - match runtime - .consume_payload_string(DEFAULT_STREAM_ID, payload) - .await - { - Ok(()) => Ok((StatusCode::ACCEPTED, Json(())).into_response()), - Err(err) => Ok(( - StatusCode::BAD_REQUEST, - Json(SdkError::internal_error().with_message(err.to_string().as_ref())), - ) - .into_response()), - } - } else { - create_sse_stream( - runtime.clone(), - session_id.clone(), - state.clone(), - Some(payload), - false, - None, - ) - .await - } - } - None => { - let error = SdkError::session_not_found(); - Ok((StatusCode::NOT_FOUND, Json(error)).into_response()) - } - } -} - -pub fn is_empty_sse_message(sse_payload: &str) -> bool { - sse_payload.is_empty() || sse_payload.trim() == ":" -} - -pub async fn delete_session( - session_id: SessionId, - state: Arc, -) -> TransportServerResult { - match state.session_store.get(&session_id).await { - Some(runtime) => { - let runtime = runtime.lock().await.to_owned(); - runtime.shutdown().await; - state.session_store.delete(&session_id).await; - tracing::info!("client disconnected : {}", &session_id); - Ok((StatusCode::OK, Json("ok")).into_response()) - } - None => { - let error = SdkError::session_not_found(); - Ok((StatusCode::NOT_FOUND, Json(error)).into_response()) - } - } -} - -pub fn acceptable_content_type(headers: &HeaderMap) -> bool { - let accept_header = headers - .get("content-type") - .and_then(|val| val.to_str().ok()) - .unwrap_or(""); - accept_header - .split(',') - .any(|val| val.trim().starts_with("application/json")) -} - -pub fn validate_mcp_protocol_version_header(headers: &HeaderMap) -> SdkResult<()> { - let protocol_version_header = headers - .get(MCP_PROTOCOL_VERSION_HEADER) - .and_then(|val| val.to_str().ok()) - .unwrap_or(""); - - // requests without protocol version header are acceptable - if protocol_version_header.is_empty() { - return Ok(()); - } - - validate_mcp_protocol_version(protocol_version_header) -} - -pub fn accepts_event_stream(headers: &HeaderMap) -> bool { - let accept_header = headers - .get("accept") - .and_then(|val| val.to_str().ok()) - .unwrap_or(""); - - accept_header - .split(',') - .any(|val| val.trim().starts_with("text/event-stream")) -} - -pub fn valid_streaming_http_accept_header(headers: &HeaderMap) -> bool { - let accept_header = headers - .get("accept") - .and_then(|val| val.to_str().ok()) - .unwrap_or(""); - - let types: Vec<_> = accept_header.split(',').map(|v| v.trim()).collect(); - - let has_event_stream = types.iter().any(|v| v.starts_with("text/event-stream")); - let has_json = types.iter().any(|v| v.starts_with("application/json")); - has_event_stream && has_json -} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes/messages_routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes/messages_routes.rs index 44b671f..65490a3 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/routes/messages_routes.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/messages_routes.rs @@ -1,54 +1,28 @@ use crate::{ - hyper_servers::{ - app_state::AppState, - error::{TransportServerError, TransportServerResult}, - }, - mcp_runtimes::server_runtime::DEFAULT_STREAM_ID, + hyper_servers::error::TransportServerResult, + mcp_http::{McpAppState, McpHttpHandler}, utils::remove_query_and_hash, }; -use axum::{ - extract::{Query, State}, - response::IntoResponse, - routing::post, - Router, -}; -use std::{collections::HashMap, sync::Arc}; +use axum::{extract::State, response::IntoResponse, routing::post, Router}; +use http::{HeaderMap, Method, Uri}; +use std::sync::Arc; -pub fn routes(state: Arc) -> Router> { +pub fn routes(sse_message_endpoint: &str) -> Router> { Router::new().route( - remove_query_and_hash(&state.sse_message_endpoint).as_str(), + remove_query_and_hash(sse_message_endpoint).as_str(), post(handle_messages), ) } pub async fn handle_messages( - State(state): State>, - Query(params): Query>, + uri: Uri, + headers: HeaderMap, + State(state): State>, message: String, ) -> TransportServerResult { - let session_id = params - .get("sessionId") - .ok_or(TransportServerError::SessionIdMissing)?; - - // transmit to the readable stream, that transport is reading from - let transmit = - state - .session_store - .get(session_id) - .await - .ok_or(TransportServerError::SessionIdInvalid( - session_id.to_string(), - ))?; - - let transmit = transmit.lock().await; - - transmit - .consume_payload_string(DEFAULT_STREAM_ID, &message) - .await - .map_err(|err| { - tracing::trace!("{}", err); - TransportServerError::StreamIoError(err.to_string()) - })?; - - Ok(axum::http::StatusCode::ACCEPTED) + let request = McpHttpHandler::create_request(Method::POST, uri, headers, Some(&message)); + let generic_response = McpHttpHandler::handle_sse_message(request, state.clone()).await?; + let (parts, body) = generic_response.into_parts(); + let resp = axum::response::Response::from_parts(parts, axum::body::Body::new(body)); + Ok(resp) } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes/sse_routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes/sse_routes.rs index 27a16b2..e13c724 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/routes/sse_routes.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/sse_routes.rs @@ -1,47 +1,10 @@ -use crate::mcp_server::error::TransportServerError; -use crate::schema::schema_utils::ClientMessage; -use crate::{ - hyper_servers::{ - app_state::AppState, - error::TransportServerResult, - middlewares::{ - protect_dns_rebinding::protect_dns_rebinding, session_id_gen::generate_session_id, - }, - }, - mcp_runtimes::server_runtime::DEFAULT_STREAM_ID, - mcp_server::{server_runtime, ServerRuntime}, - mcp_traits::mcp_handler::McpServerHandler, -}; -use axum::{ - extract::State, - middleware, - response::{ - sse::{Event, KeepAlive}, - IntoResponse, Sse, - }, - routing::get, - Extension, Router, -}; -use futures::stream::{self}; -use rust_mcp_transport::{SessionId, SseTransport}; -use std::{convert::Infallible, sync::Arc, time::Duration}; -use tokio::io::{duplex, AsyncBufReadExt, BufReader}; -use tokio_stream::StreamExt; +use crate::hyper_servers::error::TransportServerResult; +use crate::mcp_http::{McpAppState, McpHttpHandler}; +use axum::{extract::State, response::IntoResponse, routing::get, Extension, Router}; +use std::sync::Arc; -const DUPLEX_BUFFER_SIZE: usize = 8192; - -/// Creates an initial SSE event that returns the messages endpoint -/// -/// Constructs an SSE event containing the messages endpoint URL with the session ID. -/// -/// # Arguments -/// * `session_id` - The session identifier for the client -/// -/// # Returns -/// * `Result` - The constructed SSE event, infallible -fn initial_event(endpoint: &str) -> Result { - Ok(Event::default().event("endpoint").data(endpoint)) -} +#[derive(Clone)] +pub struct SseMessageEndpoint(pub String); /// Configures the SSE routes for the application /// @@ -52,18 +15,13 @@ fn initial_event(endpoint: &str) -> Result { /// * `sse_endpoint` - The path for the SSE endpoint /// /// # Returns -/// * `Router>` - An Axum router configured with the SSE route -pub fn routes(state: Arc, sse_endpoint: &str) -> Router> { - Router::new() - .route(sse_endpoint, get(handle_sse)) - .route_layer(middleware::from_fn_with_state( - state.clone(), - generate_session_id, - )) - .route_layer(middleware::from_fn_with_state( - state.clone(), - protect_dns_rebinding, - )) +/// * `Router>` - An Axum router configured with the SSE route +pub fn routes(sse_endpoint: &str, sse_message_endpoint: &str) -> Router> { + let sse_message_endpoint = SseMessageEndpoint(sse_message_endpoint.to_string()); + Router::new().route( + sse_endpoint, + get(handle_sse).layer(Extension(sse_message_endpoint)), + ) } /// Handles Server-Sent Events (SSE) connections @@ -77,91 +35,13 @@ pub fn routes(state: Arc, sse_endpoint: &str) -> Router> /// # Returns /// * `TransportServerResult` - The SSE response stream or an error pub async fn handle_sse( - Extension(session_id): Extension, - State(state): State>, + Extension(sse_message_endpoint): Extension, + State(state): State>, ) -> TransportServerResult { - let messages_endpoint = - SseTransport::::message_endpoint(&state.sse_message_endpoint, &session_id); - - // readable stream of string to be used in transport - // writing string to read_tx will be received as messages inside the transport and messages will be processed - let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); - - // writable stream to deliver message to the client - let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); - - // create a transport for sending/receiving messages - let Ok(transport) = SseTransport::new( - read_rx, - write_tx, - read_tx, - Arc::clone(&state.transport_options), - ) else { - return Err(TransportServerError::TransportError( - "Failed to create SSE transport".to_string(), - )); - }; - - let h: Arc = state.handler.clone(); - // create a new server instance with unique session_id and - let server: Arc = server_runtime::create_server_instance( - Arc::clone(&state.server_details), - h, - session_id.to_owned(), - ); - - state - .session_store - .set(session_id.to_owned(), server.clone()) - .await; - - tracing::info!("A new client joined : {}", session_id.to_owned()); - - // Start the server - tokio::spawn(async move { - match server - .start_stream( - Arc::new(transport), - DEFAULT_STREAM_ID, - state.ping_interval, - None, - ) - .await - { - Ok(_) => tracing::info!("server {} exited gracefully.", session_id.to_owned()), - Err(err) => tracing::info!( - "server {} exited with error : {}", - session_id.to_owned(), - err - ), - }; - - state.session_store.delete(&session_id).await; - }); - - // Initial SSE message to inform the client about the server's endpoint - let initial_event = stream::once(async move { initial_event(&messages_endpoint) }); - - // Construct SSE stream - let reader = BufReader::new(write_rx); - - let message_stream = stream::unfold(reader, |mut reader| async move { - let mut line = String::new(); - - match reader.read_line(&mut line).await { - Ok(0) => None, // EOF - Ok(_) => { - let trimmed_line = line.trim_end_matches('\n').to_owned(); - Some((Ok(Event::default().data(trimmed_line)), reader)) - } - Err(_) => None, // Err(e) => Some((Err(e), reader)), - } - }); - - let stream = initial_event.chain(message_stream); - let sse_stream = - Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))); - - // Return SSE response with keep-alive - Ok(sse_stream) + let SseMessageEndpoint(sse_message_endpoint) = sse_message_endpoint; + let generic_response = + McpHttpHandler::handle_sse_connection(state.clone(), Some(&sse_message_endpoint)).await?; + let (parts, body) = generic_response.into_parts(); + let resp = axum::response::Response::from_parts(parts, axum::body::Body::new(body)); + Ok(resp) } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes/streamable_http_routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes/streamable_http_routes.rs index 67f8679..6f2e470 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/routes/streamable_http_routes.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/streamable_http_routes.rs @@ -1,32 +1,16 @@ -use super::hyper_utils::start_new_session; -use crate::schema::schema_utils::SdkError; -use crate::{ - error::McpSdkError, - hyper_servers::{ - app_state::AppState, - error::TransportServerResult, - middlewares::protect_dns_rebinding::protect_dns_rebinding, - routes::hyper_utils::{ - acceptable_content_type, accepts_event_stream, create_standalone_stream, - delete_session, process_incoming_message, process_incoming_message_return, - valid_streaming_http_accept_header, validate_mcp_protocol_version_header, - }, - }, - utils::valid_initialize_method, -}; +use crate::hyper_servers::error::TransportServerResult; +use crate::mcp_http::{McpAppState, McpHttpHandler}; use axum::routing::get; use axum::{ extract::{Query, State}, - middleware, response::IntoResponse, routing::{delete, post}, - Json, Router, + Router, }; -use hyper::{HeaderMap, StatusCode}; -use rust_mcp_transport::{SessionId, MCP_LAST_EVENT_ID_HEADER, MCP_SESSION_ID_HEADER}; +use http::{HeaderMap, Method, Uri}; use std::{collections::HashMap, sync::Arc}; -pub fn routes(state: Arc, streamable_http_endpoint: &str) -> Router> { +pub fn routes(streamable_http_endpoint: &str) -> Router> { Router::new() .route(streamable_http_endpoint, get(handle_streamable_http_get)) .route(streamable_http_endpoint, post(handle_streamable_http_post)) @@ -34,129 +18,43 @@ pub fn routes(state: Arc, streamable_http_endpoint: &str) -> Router>, + uri: Uri, + State(state): State>, ) -> TransportServerResult { - if !accepts_event_stream(&headers) { - let error = SdkError::bad_request().with_message(r#"Client must accept text/event-stream"#); - return Ok((StatusCode::NOT_ACCEPTABLE, Json(error)).into_response()); - } - - if let Err(parse_error) = validate_mcp_protocol_version_header(&headers) { - let error = - SdkError::bad_request().with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); - return Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()); - } - - let session_id: Option = headers - .get(MCP_SESSION_ID_HEADER) - .and_then(|value| value.to_str().ok()) - .map(|s| s.to_string()); - - let last_event_id: Option = headers - .get(MCP_LAST_EVENT_ID_HEADER) - .and_then(|value| value.to_str().ok()) - .map(|s| s.to_string()); - - match session_id { - Some(session_id) => { - let res = create_standalone_stream(session_id, last_event_id, state).await?; - Ok(res.into_response()) - } - None => { - let error = SdkError::bad_request().with_message("Bad request: session not found"); - Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()) - } - } + let request = McpHttpHandler::create_request(Method::GET, uri, headers, None); + let generic_res = McpHttpHandler::handle_streamable_http(request, state).await?; + let (parts, body) = generic_res.into_parts(); + let resp = axum::response::Response::from_parts(parts, axum::body::Body::new(body)); + Ok(resp) } pub async fn handle_streamable_http_post( headers: HeaderMap, - State(state): State>, + uri: Uri, + State(state): State>, Query(_params): Query>, payload: String, ) -> TransportServerResult { - if !valid_streaming_http_accept_header(&headers) { - let error = SdkError::bad_request() - .with_message(r#"Client must accept both application/json and text/event-stream"#); - return Ok((StatusCode::NOT_ACCEPTABLE, Json(error)).into_response()); - } - - if !acceptable_content_type(&headers) { - let error = SdkError::bad_request() - .with_message(r#"Unsupported Media Type: Content-Type must be application/json"#); - return Ok((StatusCode::UNSUPPORTED_MEDIA_TYPE, Json(error)).into_response()); - } - - if let Err(parse_error) = validate_mcp_protocol_version_header(&headers) { - let error = - SdkError::bad_request().with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); - return Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()); - } - - let session_id: Option = headers - .get(MCP_SESSION_ID_HEADER) - .and_then(|value| value.to_str().ok()) - .map(|s| s.to_string()); - - //TODO: validate reconnect after disconnect - - match session_id { - // has session-id => write to the existing stream - Some(id) => { - if state.enable_json_response { - let res = process_incoming_message_return(id, state, &payload).await?; - Ok(res.into_response()) - } else { - let res = process_incoming_message(id, state, &payload).await?; - Ok(res.into_response()) - } - } - None => match valid_initialize_method(&payload) { - Ok(_) => { - return start_new_session(state, &payload).await; - } - Err(McpSdkError::SdkError(error)) => { - Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()) - } - Err(error) => { - let error = SdkError::bad_request().with_message(&error.to_string()); - Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()) - } - }, - } + let request = + McpHttpHandler::create_request(Method::POST, uri, headers, Some(payload.as_str())); + let generic_res = McpHttpHandler::handle_streamable_http(request, state).await?; + let (parts, body) = generic_res.into_parts(); + let resp = axum::response::Response::from_parts(parts, axum::body::Body::new(body)); + Ok(resp) } pub async fn handle_streamable_http_delete( headers: HeaderMap, - State(state): State>, + uri: Uri, + State(state): State>, ) -> TransportServerResult { - if let Err(parse_error) = validate_mcp_protocol_version_header(&headers) { - let error = - SdkError::bad_request().with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); - return Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()); - } - - let session_id: Option = headers - .get(MCP_SESSION_ID_HEADER) - .and_then(|value| value.to_str().ok()) - .map(|s| s.to_string()); - - match session_id { - Some(id) => { - let res = delete_session(id, state).await; - Ok(res.into_response()) - } - None => { - let error = SdkError::bad_request().with_message("Bad Request: Session not found"); - Ok((StatusCode::BAD_REQUEST, Json(error)).into_response()) - } - } + let request = McpHttpHandler::create_request(Method::DELETE, uri, headers, None); + let generic_res = McpHttpHandler::handle_streamable_http(request, state).await?; + let (parts, body) = generic_res.into_parts(); + let resp = axum::response::Response::from_parts(parts, axum::body::Body::new(body)); + Ok(resp) } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/server.rs b/crates/rust-mcp-sdk/src/hyper_servers/server.rs index 71bccee..4cd8eb6 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/server.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/server.rs @@ -1,8 +1,15 @@ use crate::{ error::SdkResult, id_generator::{FastIdGenerator, UuidGenerator}, + mcp_http::{ + utils::{ + DEFAULT_MESSAGES_ENDPOINT, DEFAULT_SSE_ENDPOINT, DEFAULT_STREAMABLE_HTTP_ENDPOINT, + }, + McpAppState, + }, mcp_server::hyper_runtime::HyperRuntime, mcp_traits::{mcp_handler::McpServerHandler, IdGenerator}, + session_store::InMemorySessionStore, }; #[cfg(feature = "ssl")] use axum_server::tls_rustls::RustlsConfig; @@ -16,10 +23,8 @@ use std::{ use tokio::signal; use super::{ - app_state::AppState, error::{TransportServerError, TransportServerResult}, routes::app_routes, - InMemorySessionStore, }; use crate::schema::InitializeResult; use axum::Router; @@ -28,12 +33,6 @@ use rust_mcp_transport::{event_store::EventStore, SessionId, TransportOptions}; // Default client ping interval (12 seconds) const DEFAULT_CLIENT_PING_INTERVAL: Duration = Duration::from_secs(12); const GRACEFUL_SHUTDOWN_TMEOUT_SECS: u64 = 5; -// Default Server-Sent Events (SSE) endpoint path -const DEFAULT_SSE_ENDPOINT: &str = "/sse"; -// Default MCP Messages endpoint path -const DEFAULT_MESSAGES_ENDPOINT: &str = "/messages"; -// Default Streamable HTTP endpoint path -const DEFAULT_STREAMABLE_HTTP_ENDPOINT: &str = "/mcp"; /// Configuration struct for the Hyper server /// Used to configure the HyperServer instance. @@ -237,7 +236,7 @@ impl Default for HyperServerOptions { /// Hyper server struct for managing the Axum-based web server pub struct HyperServer { app: Router, - state: Arc, + state: Arc, pub(crate) options: HyperServerOptions, handle: Handle, } @@ -259,7 +258,7 @@ impl HyperServer { handler: Arc, mut server_options: HyperServerOptions, ) -> Self { - let state: Arc = Arc::new(AppState { + let state: Arc = Arc::new(McpAppState { session_store: Arc::new(InMemorySessionStore::new()), id_generator: server_options .session_id_generator @@ -269,8 +268,6 @@ impl HyperServer { server_details: Arc::new(server_details), handler, ping_interval: server_options.ping_interval, - sse_message_endpoint: server_options.sse_messages_endpoint().to_owned(), - http_streamable_endpoint: server_options.streamable_http_endpoint().to_owned(), transport_options: Arc::clone(&server_options.transport_options), enable_json_response: server_options.enable_json_response.unwrap_or(false), allowed_hosts: server_options.allowed_hosts.take(), @@ -290,8 +287,8 @@ impl HyperServer { /// Returns a shared reference to the application state /// /// # Returns - /// * `Arc` - Shared application state - pub fn state(&self) -> Arc { + /// * `Arc` - Shared application state + pub fn state(&self) -> Arc { Arc::clone(&self.state) } @@ -451,7 +448,7 @@ impl HyperServer { } // Shutdown signal handler -async fn shutdown_signal(handle: Handle, state: Arc) { +async fn shutdown_signal(handle: Handle, state: Arc) { // Wait for a Ctrl+C or SIGTERM signal let ctrl_c = async { signal::ctrl_c() diff --git a/crates/rust-mcp-sdk/src/lib.rs b/crates/rust-mcp-sdk/src/lib.rs index a33f889..9a9e0a9 100644 --- a/crates/rust-mcp-sdk/src/lib.rs +++ b/crates/rust-mcp-sdk/src/lib.rs @@ -2,9 +2,13 @@ pub mod error; #[cfg(feature = "hyper-server")] mod hyper_servers; mod mcp_handlers; +#[cfg(feature = "hyper-server")] +pub(crate) mod mcp_http; mod mcp_macros; mod mcp_runtimes; mod mcp_traits; +#[cfg(any(feature = "server", feature = "hyper-server"))] +pub mod session_store; mod utils; #[cfg(feature = "client")] @@ -63,7 +67,7 @@ pub mod mcp_server { //! handle each message based on its type and parameters. //! //! Refer to [examples/hello-world-mcp-server-stdio-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio-core) for an example. - pub use super::mcp_handlers::mcp_server_handler::ServerHandler; + pub use super::mcp_handlers::mcp_server_handler::{ServerHandler, ToMcpServerHandler}; pub use super::mcp_handlers::mcp_server_handler_core::ServerHandlerCore; pub use super::mcp_runtimes::server_runtime::mcp_server_runtime as server_runtime; @@ -77,6 +81,9 @@ pub mod mcp_server { #[cfg(feature = "hyper-server")] pub use super::hyper_servers::*; pub use super::utils::enforce_compatible_protocol_version; + + #[cfg(feature = "hyper-server")] + pub use super::mcp_http::{McpAppState, McpHttpHandler}; } #[cfg(feature = "client")] diff --git a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs index 9b9577e..9f8c9e3 100644 --- a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs +++ b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs @@ -1,4 +1,8 @@ -use crate::schema::{schema_utils::CallToolError, *}; +use crate::{ + mcp_server::server_runtime::ServerRuntimeInternalHandler, + mcp_traits::mcp_handler::McpServerHandler, + schema::{schema_utils::CallToolError, *}, +}; use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; @@ -326,3 +330,14 @@ pub trait ServerHandler: Send + Sync + 'static { Ok(()) } } + +// Custom trait for conversion +pub trait ToMcpServerHandler { + fn to_mcp_server_handler(self) -> Arc; +} + +impl ToMcpServerHandler for T { + fn to_mcp_server_handler(self) -> Arc { + Arc::new(ServerRuntimeInternalHandler::new(Box::new(self))) + } +} diff --git a/crates/rust-mcp-sdk/src/mcp_http.rs b/crates/rust-mcp-sdk/src/mcp_http.rs new file mode 100644 index 0000000..3f443d5 --- /dev/null +++ b/crates/rust-mcp-sdk/src/mcp_http.rs @@ -0,0 +1,10 @@ +mod app_state; +mod mcp_http_handler; +pub(crate) mod mcp_http_utils; + +pub use app_state::*; +pub use mcp_http_handler::*; + +pub(crate) mod utils { + pub use super::mcp_http_utils::*; +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs b/crates/rust-mcp-sdk/src/mcp_http/app_state.rs similarity index 89% rename from crates/rust-mcp-sdk/src/hyper_servers/app_state.rs rename to crates/rust-mcp-sdk/src/mcp_http/app_state.rs index f96b261..cada97d 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs +++ b/crates/rust-mcp-sdk/src/mcp_http/app_state.rs @@ -1,27 +1,22 @@ -use std::{sync::Arc, time::Duration}; - -use super::session_store::SessionStore; use crate::mcp_traits::mcp_handler::McpServerHandler; +use crate::session_store::SessionStore; use crate::{id_generator::FastIdGenerator, mcp_traits::IdGenerator, schema::InitializeResult}; - use rust_mcp_transport::event_store::EventStore; - use rust_mcp_transport::{SessionId, TransportOptions}; +use std::{sync::Arc, time::Duration}; -/// Application state struct for the Hyper server +/// Application state struct for the Hyper ser /// /// Holds shared, thread-safe references to session storage, ID generator, /// server details, handler, ping interval, and transport options. #[derive(Clone)] -pub struct AppState { +pub struct McpAppState { pub session_store: Arc, pub id_generator: Arc>, pub stream_id_gen: Arc, pub server_details: Arc, pub handler: Arc, pub ping_interval: Duration, - pub sse_message_endpoint: String, - pub http_streamable_endpoint: String, pub transport_options: Arc, pub enable_json_response: bool, /// List of allowed host header values for DNS rebinding protection. @@ -38,7 +33,7 @@ pub struct AppState { pub event_store: Option>, } -impl AppState { +impl McpAppState { pub fn needs_dns_protection(&self) -> bool { self.dns_rebinding_protection && (self.allowed_hosts.is_some() || self.allowed_origins.is_some()) diff --git a/crates/rust-mcp-sdk/src/mcp_http/mcp_http_handler.rs b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_handler.rs new file mode 100644 index 0000000..8b7efcf --- /dev/null +++ b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_handler.rs @@ -0,0 +1,305 @@ +#[cfg(feature = "sse")] +use super::utils::handle_sse_connection; +use crate::mcp_http::utils::{ + accepts_event_stream, error_response, query_param, validate_mcp_protocol_version_header, +}; +use crate::mcp_runtimes::server_runtime::DEFAULT_STREAM_ID; +use crate::mcp_server::error::TransportServerError; +use crate::schema::schema_utils::SdkError; +use crate::{ + error::McpSdkError, + mcp_http::{ + utils::{ + acceptable_content_type, create_standalone_stream, delete_session, + process_incoming_message, process_incoming_message_return, protect_dns_rebinding, + start_new_session, valid_streaming_http_accept_header, GenericBody, + }, + McpAppState, + }, + mcp_server::error::TransportServerResult, + utils::valid_initialize_method, +}; +use bytes::Bytes; +use http::{self, HeaderMap, Method, StatusCode, Uri}; +use http_body_util::{BodyExt, Full}; +use rust_mcp_transport::{SessionId, MCP_LAST_EVENT_ID_HEADER, MCP_SESSION_ID_HEADER}; +use std::sync::Arc; + +pub struct McpHttpHandler {} + +impl McpHttpHandler { + /// Creates a new HTTP request with the given method, URI, headers, and optional body. + /// + /// # Arguments + /// + /// * `method` - The HTTP method to use (e.g., GET, POST). + /// * `uri` - The target URI for the request. + /// * `headers` - A map of optional header keys and their corresponding values. + /// * `body` - An optional string slice representing the request body. + /// + /// # Returns + /// + /// An `http::Request<&str>` initialized with the specified method, URI, headers, and body. + /// If the `body` is `None`, an empty string is used as the default. + /// + pub fn create_request( + method: Method, + uri: Uri, + headers: HeaderMap, + body: Option<&str>, + ) -> http::Request<&str> { + let mut request = http::Request::default(); + *request.method_mut() = method; + *request.uri_mut() = uri; + *request.body_mut() = body.unwrap_or_default(); + let req_headers = request.headers_mut(); + for (key, value) in headers { + if let Some(k) = key { + req_headers.insert(k, value); + } + } + request + } +} + +impl McpHttpHandler { + /// Handles an MCP connection using the SSE (Server-Sent Events) transport. + /// + /// This function serves as the entry point for initializing and managing a client connection + /// over SSE when the `sse` feature is enabled. + /// + /// # Arguments + /// * `state` - Shared application state required to manage the MCP session. + /// * `sse_message_endpoint` - Optional message endpoint to override the default SSE route (default: `/messages` ). + /// + /// + /// # Features + /// This function is only available when the `sse` feature is enabled. + #[cfg(feature = "sse")] + pub async fn handle_sse_connection( + state: Arc, + sse_message_endpoint: Option<&str>, + ) -> TransportServerResult> { + handle_sse_connection(state, sse_message_endpoint).await + } + + /// Handles incoming MCP messages from the client after an SSE connection is established. + /// + /// This function processes a message sent by the client as part of an active SSE session. It: + /// - Extracts the `sessionId` from the request query parameters. + /// - Locates the corresponding session's transmit channel. + /// - Forwards the incoming message payload to the MCP transport stream for consumption. + /// # Arguments + /// * `request` - The HTTP request containing the message body and query parameters (including `sessionId`). + /// * `state` - Shared application state, including access to the session store. + /// + /// # Returns + /// * `TransportServerResult>`: + /// - Returns a `202 Accepted` HTTP response if the message is successfully forwarded. + /// - Returns an error if the session ID is missing, invalid, or if any I/O issues occur while processing the message. + /// + /// # Errors + /// - `SessionIdMissing`: if the `sessionId` query parameter is not present. + /// - `SessionIdInvalid`: if the session ID does not map to a valid session in the session store. + /// - `StreamIoError`: if an error occurs while writing to the stream. + /// - `HttpError`: if constructing the HTTP response fails. + pub async fn handle_sse_message( + request: http::Request<&str>, + state: Arc, + ) -> TransportServerResult> { + let session_id = + query_param(&request, "sessionId").ok_or(TransportServerError::SessionIdMissing)?; + + // transmit to the readable stream, that transport is reading from + let transmit = state.session_store.get(&session_id).await.ok_or( + TransportServerError::SessionIdInvalid(session_id.to_string()), + )?; + + let message = *request.body(); + transmit + .consume_payload_string(DEFAULT_STREAM_ID, message) + .await + .map_err(|err| { + tracing::trace!("{}", err); + TransportServerError::StreamIoError(err.to_string()) + })?; + + let body = Full::new(Bytes::new()) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + + http::Response::builder() + .status(StatusCode::ACCEPTED) + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + + /// Handles incoming MCP messages over the StreamableHTTP transport. + /// + /// It supports `GET`, `POST`, and `DELETE` methods for handling streaming operations, and performs optional + /// DNS rebinding protection if it is configured. + /// + /// # Arguments + /// * `request` - The HTTP request from the client, including method, headers, and optional body. + /// * `state` - Shared application state, including configuration and session management. + /// + /// # Behavior + /// - If DNS rebinding protection is enabled via the app state, the function checks the request headers. + /// If dns protection fails, a `403 Forbidden` response is returned. + /// - Dispatches the request to method-specific handlers based on the HTTP method: + /// - `GET` β†’ `handle_http_get` + /// - `POST` β†’ `handle_http_post` + /// - `DELETE` β†’ `handle_http_delete` + /// - Returns `405 Method Not Allowed` for unsupported methods. + /// + /// # Returns + /// * A `TransportServerResult` wrapping an HTTP response indicating success or failure of the operation. + /// + pub async fn handle_streamable_http( + request: http::Request<&str>, + state: Arc, + ) -> TransportServerResult> { + // Enforces DNS rebinding protection if required by state. + // If protection fails, respond with HTTP 403 Forbidden. + if state.needs_dns_protection() { + if let Err(error) = protect_dns_rebinding(request.headers(), state.clone()).await { + return error_response(StatusCode::FORBIDDEN, error); + } + } + + let method = request.method(); + match method { + &http::Method::GET => return Self::handle_http_get(request, state).await, + &http::Method::POST => return Self::handle_http_post(request, state).await, + &http::Method::DELETE => return Self::handle_http_delete(request, state).await, + other => { + let error = SdkError::bad_request().with_message(&format!( + "'{other}' is not a valid HTTP method for StreamableHTTP transport." + )); + error_response(StatusCode::METHOD_NOT_ALLOWED, error) + } + } + } + + /// Processes POST requests for the Streamable HTTP Protocol + async fn handle_http_post( + request: http::Request<&str>, + state: Arc, + ) -> TransportServerResult> { + let headers = request.headers(); + + if !valid_streaming_http_accept_header(headers) { + let error = SdkError::bad_request() + .with_message(r#"Client must accept both application/json and text/event-stream"#); + return error_response(StatusCode::NOT_ACCEPTABLE, error); + } + + if !acceptable_content_type(headers) { + let error = SdkError::bad_request() + .with_message(r#"Unsupported Media Type: Content-Type must be application/json"#); + return error_response(StatusCode::UNSUPPORTED_MEDIA_TYPE, error); + } + + if let Err(parse_error) = validate_mcp_protocol_version_header(headers) { + let error = SdkError::bad_request() + .with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); + return error_response(StatusCode::BAD_REQUEST, error); + } + + let session_id: Option = headers + .get(MCP_SESSION_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(|s| s.to_string()); + + let payload = *request.body(); + + match session_id { + // has session-id => write to the existing stream + Some(id) => { + if state.enable_json_response { + process_incoming_message_return(id, state, payload).await + } else { + process_incoming_message(id, state, payload).await + } + } + None => match valid_initialize_method(payload) { + Ok(_) => { + return start_new_session(state, payload).await; + } + Err(McpSdkError::SdkError(error)) => error_response(StatusCode::BAD_REQUEST, error), + Err(error) => { + let error = SdkError::bad_request().with_message(&error.to_string()); + error_response(StatusCode::BAD_REQUEST, error) + } + }, + } + } + + /// Processes GET requests for the Streamable HTTP Protocol + async fn handle_http_get( + request: http::Request<&str>, + state: Arc, + ) -> TransportServerResult> { + let headers = request.headers(); + + if !accepts_event_stream(headers) { + let error = + SdkError::bad_request().with_message(r#"Client must accept text/event-stream"#); + return error_response(StatusCode::NOT_ACCEPTABLE, error); + } + + if let Err(parse_error) = validate_mcp_protocol_version_header(headers) { + let error = SdkError::bad_request() + .with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); + return error_response(StatusCode::BAD_REQUEST, error); + } + + let session_id: Option = headers + .get(MCP_SESSION_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(|s| s.to_string()); + + let last_event_id: Option = headers + .get(MCP_LAST_EVENT_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(|s| s.to_string()); + + match session_id { + Some(session_id) => { + let res = create_standalone_stream(session_id, last_event_id, state).await; + res + } + None => { + let error = SdkError::bad_request().with_message("Bad request: session not found"); + error_response(StatusCode::BAD_REQUEST, error) + } + } + } + + /// Processes DELETE requests for the Streamable HTTP Protocol + async fn handle_http_delete( + request: http::Request<&str>, + state: Arc, + ) -> TransportServerResult> { + let headers = request.headers(); + + if let Err(parse_error) = validate_mcp_protocol_version_header(headers) { + let error = SdkError::bad_request() + .with_message(format!(r#"Bad Request: {parse_error}"#).as_str()); + return error_response(StatusCode::BAD_REQUEST, error); + } + + let session_id: Option = headers + .get(MCP_SESSION_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(|s| s.to_string()); + + match session_id { + Some(id) => delete_session(id, state).await, + None => { + let error = SdkError::bad_request().with_message("Bad Request: Session not found"); + error_response(StatusCode::BAD_REQUEST, error) + } + } + } +} diff --git a/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs new file mode 100644 index 0000000..608207a --- /dev/null +++ b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs @@ -0,0 +1,754 @@ +use crate::schema::schema_utils::{ClientMessage, SdkError}; +use crate::{ + error::SdkResult, + hyper_servers::error::{TransportServerError, TransportServerResult}, + mcp_http::McpAppState, + mcp_runtimes::server_runtime::DEFAULT_STREAM_ID, + mcp_server::{server_runtime, ServerRuntime}, + mcp_traits::{mcp_handler::McpServerHandler, IdGenerator}, + utils::validate_mcp_protocol_version, +}; +use axum::http::HeaderValue; +use bytes::Bytes; +use futures::stream; +use http::header::{ACCEPT, CONNECTION, CONTENT_TYPE, HOST, ORIGIN}; +use http_body::Frame; +use http_body_util::StreamBody; +use http_body_util::{combinators::BoxBody, BodyExt, Full}; +use hyper::{HeaderMap, StatusCode}; +use rust_mcp_transport::{ + EventId, McpDispatch, SessionId, SseEvent, SseTransport, StreamId, ID_SEPARATOR, + MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_ID_HEADER, +}; +use std::sync::Arc; +use tokio::io::{duplex, AsyncBufReadExt, BufReader}; +use tokio_stream::StreamExt; + +// Default Server-Sent Events (SSE) endpoint path +pub(crate) const DEFAULT_SSE_ENDPOINT: &str = "/sse"; +// Default MCP Messages endpoint path +pub(crate) const DEFAULT_MESSAGES_ENDPOINT: &str = "/messages"; +// Default Streamable HTTP endpoint path +pub(crate) const DEFAULT_STREAMABLE_HTTP_ENDPOINT: &str = "/mcp"; +const DUPLEX_BUFFER_SIZE: usize = 8192; + +pub type GenericBody = BoxBody; + +/// Creates an initial SSE event that returns the messages endpoint +/// +/// Constructs an SSE event containing the messages endpoint URL with the session ID. +/// +/// # Arguments +/// * `session_id` - The session identifier for the client +/// +/// # Returns +/// * `Result` - The constructed SSE event, infallible +fn initial_sse_event(endpoint: &str) -> Result { + Ok(SseEvent::default() + .with_event("endpoint") + .with_data(endpoint.to_string()) + .as_bytes()) +} + +async fn create_sse_stream( + runtime: Arc, + session_id: SessionId, + state: Arc, + payload: Option<&str>, + standalone: bool, + last_event_id: Option, +) -> TransportServerResult> { + let payload_string = payload.map(|p| p.to_string()); + + // TODO: this logic should be moved out after refactoing the mcp_stream.rs + let payload_contains_request = payload_string + .as_ref() + .map(|json_str| contains_request(json_str)) + .unwrap_or(Ok(false)); + let Ok(payload_contains_request) = payload_contains_request else { + return error_response(StatusCode::BAD_REQUEST, SdkError::parse_error()); + }; + + // readable stream of string to be used in transport + let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); + // writable stream to deliver message to the client + let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); + + let session_id = Arc::new(session_id); + let stream_id: Arc = if standalone { + Arc::new(DEFAULT_STREAM_ID.to_string()) + } else { + Arc::new(state.stream_id_gen.generate()) + }; + + let event_store = state.event_store.as_ref().map(Arc::clone); + let resumability_enabled = event_store.is_some(); + + let mut transport = SseTransport::::new( + read_rx, + write_tx, + read_tx, + Arc::clone(&state.transport_options), + ) + .map_err(|err| TransportServerError::TransportError(err.to_string()))?; + if let Some(event_store) = event_store.clone() { + transport.make_resumable((*session_id).clone(), (*stream_id).clone(), event_store); + } + let transport = Arc::new(transport); + + let ping_interval = state.ping_interval; + let runtime_clone = Arc::clone(&runtime); + let stream_id_clone = stream_id.clone(); + let transport_clone = transport.clone(); + + //Start the server runtime + tokio::spawn(async move { + match runtime_clone + .start_stream( + transport_clone, + &stream_id_clone, + ping_interval, + payload_string, + ) + .await + { + Ok(_) => tracing::trace!("stream {} exited gracefully.", &stream_id_clone), + Err(err) => tracing::info!("stream {} exited with error : {}", &stream_id_clone, err), + } + let _ = runtime.remove_transport(&stream_id_clone).await; + }); + + // Construct SSE stream + let reader = BufReader::new(write_rx); + + // send outgoing messages from server to the client over the sse stream + let message_stream = stream::unfold(reader, move |mut reader| { + async move { + let mut line = String::new(); + + match reader.read_line(&mut line).await { + Ok(0) => None, // EOF + Ok(_) => { + let trimmed_line = line.trim_end_matches('\n').to_owned(); + + // empty sse comment to keep-alive + if is_empty_sse_message(&trimmed_line) { + return Some((Ok(SseEvent::default().as_bytes()), reader)); + } + + let (event_id, message) = match ( + resumability_enabled, + trimmed_line.split_once(char::from(ID_SEPARATOR)), + ) { + (true, Some((id, msg))) => (Some(id.to_string()), msg.to_string()), + _ => (None, trimmed_line), + }; + + let event = match event_id { + Some(id) => SseEvent::default() + .with_data(message) + .with_id(id) + .as_bytes(), + None => SseEvent::default().with_data(message).as_bytes(), + }; + + Some((Ok(event), reader)) + } + Err(e) => Some((Err(e), reader)), + } + } + }); + + // create a stream body + let streaming_body: GenericBody = + http_body_util::BodyExt::boxed(StreamBody::new(message_stream.map(|res| { + res.map(Frame::data) + .map_err(|err: std::io::Error| TransportServerError::HttpError(err.to_string())) + }))); + + let session_id_value = HeaderValue::from_str(&session_id) + .map_err(|err| TransportServerError::HttpError(err.to_string()))?; + + let status_code = if !payload_contains_request { + StatusCode::ACCEPTED + } else { + StatusCode::OK + }; + + let response = http::Response::builder() + .status(status_code) + .header(CONTENT_TYPE, "text/event-stream") + .header(MCP_SESSION_ID_HEADER, session_id_value) + .header(CONNECTION, "keep-alive") + .body(streaming_body) + .map_err(|err| TransportServerError::HttpError(err.to_string()))?; + + // if last_event_id exists we replay messages from the event-store + tokio::spawn(async move { + if let Some(last_event_id) = last_event_id { + if let Some(event_store) = state.event_store.as_ref() { + if let Some(events) = event_store.events_after(last_event_id).await { + for message_payload in events.messages { + // skip storing replay messages + let error = transport.write_str(&message_payload, true).await; + if let Err(error) = error { + tracing::trace!("Error replaying message: {error}") + } + } + } + } + } + }); + + Ok(response) +} + +// TODO: this function will be removed after refactoring the readable stream of the transports +// so we would deserialize the string syncronousely and have more control over the flow +// this function may incur a slight runtime cost which could be avoided after refactoring +fn contains_request(json_str: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(json_str)?; + match value { + serde_json::Value::Object(obj) => Ok(obj.contains_key("id") && obj.contains_key("method")), + serde_json::Value::Array(arr) => Ok(arr.iter().any(|item| { + item.as_object() + .map(|obj| obj.contains_key("id") && obj.contains_key("method")) + .unwrap_or(false) + })), + _ => Ok(false), + } +} + +fn is_result(json_str: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(json_str)?; + match value { + serde_json::Value::Object(obj) => Ok(obj.contains_key("result")), + serde_json::Value::Array(arr) => Ok(arr.iter().all(|item| { + item.as_object() + .map(|obj| obj.contains_key("result")) + .unwrap_or(false) + })), + _ => Ok(false), + } +} + +pub async fn create_standalone_stream( + session_id: SessionId, + last_event_id: Option, + state: Arc, +) -> TransportServerResult> { + let runtime = state.session_store.get(&session_id).await.ok_or( + TransportServerError::SessionIdInvalid(session_id.to_string()), + )?; + + if runtime.stream_id_exists(DEFAULT_STREAM_ID).await { + let error = + SdkError::bad_request().with_message("Only one SSE stream is allowed per session"); + return error_response(StatusCode::CONFLICT, error) + .map_err(|err| TransportServerError::HttpError(err.to_string())); + } + + if let Some(last_event_id) = last_event_id.as_ref() { + tracing::trace!( + "SSE stream re-connected with last-event-id: {}", + last_event_id + ); + } + + let mut response = create_sse_stream( + runtime.clone(), + session_id.clone(), + state.clone(), + None, + true, + last_event_id, + ) + .await?; + *response.status_mut() = StatusCode::OK; + Ok(response) +} + +pub async fn start_new_session( + state: Arc, + payload: &str, +) -> TransportServerResult> { + let session_id: SessionId = state.id_generator.generate(); + + let h: Arc = state.handler.clone(); + // create a new server instance with unique session_id and + let runtime: Arc = server_runtime::create_server_instance( + Arc::clone(&state.server_details), + h, + session_id.to_owned(), + ); + + tracing::info!("a new client joined : {}", &session_id); + + let response = create_sse_stream( + runtime.clone(), + session_id.clone(), + state.clone(), + Some(payload), + false, + None, + ) + .await; + + if response.is_ok() { + state + .session_store + .set(session_id.to_owned(), runtime.clone()) + .await; + } + response +} +async fn single_shot_stream( + runtime: Arc, + session_id: SessionId, + state: Arc, + payload: Option<&str>, + standalone: bool, +) -> TransportServerResult> { + // readable stream of string to be used in transport + let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); + // writable stream to deliver message to the client + let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); + + let transport = SseTransport::::new( + read_rx, + write_tx, + read_tx, + Arc::clone(&state.transport_options), + ) + .map_err(|err| TransportServerError::TransportError(err.to_string()))?; + + let stream_id = if standalone { + DEFAULT_STREAM_ID.to_string() + } else { + state.id_generator.generate() + }; + let ping_interval = state.ping_interval; + let runtime_clone = Arc::clone(&runtime); + + let payload_string = payload.map(|p| p.to_string()); + + tokio::spawn(async move { + match runtime_clone + .start_stream( + Arc::new(transport), + &stream_id, + ping_interval, + payload_string, + ) + .await + { + Ok(_) => tracing::info!("stream {} exited gracefully.", &stream_id), + Err(err) => tracing::info!("stream {} exited with error : {}", &stream_id, err), + } + let _ = runtime.remove_transport(&stream_id).await; + }); + + let mut reader = BufReader::new(write_rx); + let mut line = String::new(); + let response = match reader.read_line(&mut line).await { + Ok(0) => None, // EOF + Ok(_) => { + let trimmed_line = line.trim_end_matches('\n').to_owned(); + Some(Ok(trimmed_line)) + } + Err(e) => Some(Err(e)), + }; + + let session_id_value = HeaderValue::from_str(&session_id) + .map_err(|err| TransportServerError::HttpError(err.to_string()))?; + + match response { + Some(response_result) => match response_result { + Ok(response_str) => { + let body = Full::new(Bytes::from(response_str)) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + + http::Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/json") + .header(MCP_SESSION_ID_HEADER, session_id_value) + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + Err(err) => { + let body = Full::new(Bytes::from(err.to_string())) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, "application/json") + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + }, + None => { + let body = Full::new(Bytes::from( + "End of the transport stream reached.".to_string(), + )) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + http::Response::builder() + .status(StatusCode::UNPROCESSABLE_ENTITY) + .header(CONTENT_TYPE, "application/json") + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + } +} + +pub async fn process_incoming_message_return( + session_id: SessionId, + state: Arc, + payload: &str, +) -> TransportServerResult> { + match state.session_store.get(&session_id).await { + Some(runtime) => { + single_shot_stream( + runtime.clone(), + session_id, + state.clone(), + Some(payload), + false, + ) + .await + // Ok(StatusCode::OK.into_response()) + } + None => { + let error = SdkError::session_not_found(); + error_response(StatusCode::NOT_FOUND, error) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + } +} + +pub async fn process_incoming_message( + session_id: SessionId, + state: Arc, + payload: &str, +) -> TransportServerResult> { + match state.session_store.get(&session_id).await { + Some(runtime) => { + // when receiving a result in a streamable_http server, that means it was sent by the standalone sse transport + // it should be processed by the same transport , therefore no need to call create_sse_stream + let Ok(is_result) = is_result(payload) else { + return error_response(StatusCode::BAD_REQUEST, SdkError::parse_error()); + }; + + if is_result { + match runtime + .consume_payload_string(DEFAULT_STREAM_ID, payload) + .await + { + Ok(()) => { + let body = Full::new(Bytes::new()) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + http::Response::builder() + .status(200) + .header("Content-Type", "application/json") + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + Err(err) => { + let error = + SdkError::internal_error().with_message(err.to_string().as_ref()); + error_response(StatusCode::BAD_REQUEST, error) + } + } + } else { + create_sse_stream( + runtime.clone(), + session_id.clone(), + state.clone(), + Some(payload), + false, + None, + ) + .await + } + } + None => { + let error = SdkError::session_not_found(); + error_response(StatusCode::NOT_FOUND, error) + } + } +} + +pub fn is_empty_sse_message(sse_payload: &str) -> bool { + sse_payload.is_empty() || sse_payload.trim() == ":" +} + +pub async fn delete_session( + session_id: SessionId, + state: Arc, +) -> TransportServerResult> { + match state.session_store.get(&session_id).await { + Some(runtime) => { + runtime.shutdown().await; + state.session_store.delete(&session_id).await; + tracing::info!("client disconnected : {}", &session_id); + + let body = Full::new(Bytes::from("ok")) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + http::Response::builder() + .status(200) + .header("Content-Type", "application/json") + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + } + None => { + let error = SdkError::session_not_found(); + error_response(StatusCode::NOT_FOUND, error) + } + } +} + +pub fn acceptable_content_type(headers: &HeaderMap) -> bool { + let accept_header = headers + .get("content-type") + .and_then(|val| val.to_str().ok()) + .unwrap_or(""); + accept_header + .split(',') + .any(|val| val.trim().starts_with("application/json")) +} + +pub fn validate_mcp_protocol_version_header(headers: &HeaderMap) -> SdkResult<()> { + let protocol_version_header = headers + .get(MCP_PROTOCOL_VERSION_HEADER) + .and_then(|val| val.to_str().ok()) + .unwrap_or(""); + + // requests without protocol version header are acceptable + if protocol_version_header.is_empty() { + return Ok(()); + } + + validate_mcp_protocol_version(protocol_version_header) +} + +pub fn accepts_event_stream(headers: &HeaderMap) -> bool { + let accept_header = headers + .get(ACCEPT) + .and_then(|val| val.to_str().ok()) + .unwrap_or(""); + + accept_header + .split(',') + .any(|val| val.trim().starts_with("text/event-stream")) +} + +pub fn valid_streaming_http_accept_header(headers: &HeaderMap) -> bool { + let accept_header = headers + .get(ACCEPT) + .and_then(|val| val.to_str().ok()) + .unwrap_or(""); + + let types: Vec<_> = accept_header.split(',').map(|v| v.trim()).collect(); + + let has_event_stream = types.iter().any(|v| v.starts_with("text/event-stream")); + let has_json = types.iter().any(|v| v.starts_with("application/json")); + has_event_stream && has_json +} + +pub fn error_response( + status_code: StatusCode, + error: SdkError, +) -> TransportServerResult> { + let error_string = serde_json::to_string(&error).unwrap_or_default(); + let body = Full::new(Bytes::from(error_string)) + .map_err(|err| TransportServerError::HttpError(err.to_string())) + .boxed(); + + http::Response::builder() + .status(status_code) + .header(CONTENT_TYPE, "application/json") + .body(body) + .map_err(|err| TransportServerError::HttpError(err.to_string())) +} + +// Protect against DNS rebinding attacks by validating Host and Origin headers. +pub(crate) async fn protect_dns_rebinding( + headers: &http::HeaderMap, + state: Arc, +) -> Result<(), SdkError> { + if !state.needs_dns_protection() { + // If protection is not needed, pass the request to the next handler + return Ok(()); + } + + if let Some(allowed_hosts) = state.allowed_hosts.as_ref() { + if !allowed_hosts.is_empty() { + let Some(host) = headers.get(HOST).and_then(|h| h.to_str().ok()) else { + return Err(SdkError::bad_request().with_message("Invalid Host header: [unknown] ")); + }; + + if !allowed_hosts + .iter() + .any(|allowed| allowed.eq_ignore_ascii_case(host)) + { + return Err(SdkError::bad_request() + .with_message(format!("Invalid Host header: \"{host}\" ").as_str())); + } + } + } + + if let Some(allowed_origins) = state.allowed_origins.as_ref() { + if !allowed_origins.is_empty() { + let Some(origin) = headers.get(ORIGIN).and_then(|h| h.to_str().ok()) else { + return Err( + SdkError::bad_request().with_message("Invalid Origin header: [unknown] ") + ); + }; + + if !allowed_origins + .iter() + .any(|allowed| allowed.eq_ignore_ascii_case(origin)) + { + return Err(SdkError::bad_request() + .with_message(format!("Invalid Origin header: \"{origin}\" ").as_str())); + } + } + } + + Ok(()) +} + +/// Extracts the value of a query parameter from an HTTP request by key. +/// +/// This function parses the query string from the request URI and searches +/// for the specified key. If found, it returns the corresponding value as a `String`. +/// +/// # Arguments +/// * `request` - The HTTP request containing the URI with the query string. +/// * `key` - The name of the query parameter to retrieve. +/// +/// # Returns +/// * `Some(String)` containing the value of the query parameter if found. +/// * `None` if the query string is missing or the key is not present. +/// +pub fn query_param(request: &http::Request<&str>, key: &str) -> Option { + request.uri().query().and_then(|query| { + for pair in query.split('&') { + let mut split = pair.splitn(2, '='); + let k = split.next()?; + let v = split.next().unwrap_or(""); + if k == key { + return Some(v.to_string()); + } + } + None + }) +} + +#[cfg(feature = "sse")] +pub(crate) async fn handle_sse_connection( + state: Arc, + sse_message_endpoint: Option<&str>, +) -> TransportServerResult> { + let session_id: SessionId = state.id_generator.generate(); + + let sse_message_endpoint = sse_message_endpoint.unwrap_or(DEFAULT_MESSAGES_ENDPOINT); + let messages_endpoint = + SseTransport::::message_endpoint(sse_message_endpoint, &session_id); + + // readable stream of string to be used in transport + // writing string to read_tx will be received as messages inside the transport and messages will be processed + let (read_tx, read_rx) = duplex(DUPLEX_BUFFER_SIZE); + + // writable stream to deliver message to the client + let (write_tx, write_rx) = duplex(DUPLEX_BUFFER_SIZE); + + // / create a transport for sending/receiving messages + let Ok(transport) = SseTransport::new( + read_rx, + write_tx, + read_tx, + Arc::clone(&state.transport_options), + ) else { + return Err(TransportServerError::TransportError( + "Failed to create SSE transport".to_string(), + )); + }; + + let h: Arc = state.handler.clone(); + // create a new server instance with unique session_id and + let server: Arc = server_runtime::create_server_instance( + Arc::clone(&state.server_details), + h, + session_id.to_owned(), + ); + + state + .session_store + .set(session_id.to_owned(), server.clone()) + .await; + + tracing::info!("A new client joined : {}", session_id.to_owned()); + + // Start the server + tokio::spawn(async move { + match server + .start_stream( + Arc::new(transport), + DEFAULT_STREAM_ID, + state.ping_interval, + None, + ) + .await + { + Ok(_) => tracing::info!("server {} exited gracefully.", session_id.to_owned()), + Err(err) => tracing::info!( + "server {} exited with error : {}", + session_id.to_owned(), + err + ), + }; + + state.session_store.delete(&session_id).await; + }); + + // Initial SSE message to inform the client about the server's endpoint + let initial_sse_event = stream::once(async move { initial_sse_event(&messages_endpoint) }); + + // Construct SSE stream + let reader = BufReader::new(write_rx); + + let message_stream = stream::unfold(reader, |mut reader| async move { + let mut line = String::new(); + + match reader.read_line(&mut line).await { + Ok(0) => None, // EOF + Ok(_) => { + let trimmed_line = line.trim_end_matches('\n').to_owned(); + Some(( + Ok(SseEvent::default().with_data(trimmed_line).as_bytes()), + reader, + )) + } + Err(_) => None, // Err(e) => Some((Err(e), reader)), + } + }); + + let stream = initial_sse_event.chain(message_stream); + + // create a stream body + let streaming_body: GenericBody = + http_body_util::BodyExt::boxed(StreamBody::new(stream.map(|res| res.map(Frame::data)))); + + let response = http::Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/event-stream") + .header(CONNECTION, "keep-alive") + .body(streaming_body) + .map_err(|err| TransportServerError::HttpError(err.to_string()))?; + + Ok(response) +} diff --git a/crates/rust-mcp-sdk/src/mcp_traits/mcp_client.rs b/crates/rust-mcp-sdk/src/mcp_traits/mcp_client.rs index 5fe3fba..c295082 100644 --- a/crates/rust-mcp-sdk/src/mcp_traits/mcp_client.rs +++ b/crates/rust-mcp-sdk/src/mcp_traits/mcp_client.rs @@ -131,6 +131,22 @@ pub trait McpClient: Sync + Send { .map(|server_details| server_details.capabilities.logging.is_some()) } + /// Checks if the server supports argument autocompletion suggestions. + /// + /// This function retrieves the server information and checks if the + /// server has completions capabilities listed. If the server info has + /// not been retrieved yet, it returns `None`. Otherwise, it returns + /// `Some(true)` if completions is supported, or `Some(false)` if not. + /// + /// # Returns + /// - `None` if server information is not yet available. + /// - `Some(true)` if completions is supported by the server. + /// - `Some(false)` if completions is not supported by the server. + fn server_supports_completion(&self) -> Option { + self.server_info() + .map(|server_details| server_details.capabilities.completions.is_some()) + } + fn instructions(&self) -> Option { self.server_info()?.instructions } diff --git a/crates/rust-mcp-sdk/src/schema.rs b/crates/rust-mcp-sdk/src/schema.rs index 2c7e7b4..bc008c2 100644 --- a/crates/rust-mcp-sdk/src/schema.rs +++ b/crates/rust-mcp-sdk/src/schema.rs @@ -1,6 +1,9 @@ #[cfg(feature = "2025_06_18")] pub use rust_mcp_schema::*; +#[cfg(not(feature = "2025_06_18"))] +pub use rust_mcp_schema::{ParseProtocolVersionError, ProtocolVersion}; + #[cfg(all( feature = "2025_03_26", not(any(feature = "2024_11_05", feature = "2025_06_18")) diff --git a/crates/rust-mcp-sdk/src/hyper_servers/session_store.rs b/crates/rust-mcp-sdk/src/session_store.rs similarity index 69% rename from crates/rust-mcp-sdk/src/hyper_servers/session_store.rs rename to crates/rust-mcp-sdk/src/session_store.rs index 4384b1a..80c4087 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/session_store.rs +++ b/crates/rust-mcp-sdk/src/session_store.rs @@ -1,15 +1,9 @@ -mod in_memory; -use std::sync::Arc; - +mod in_memory_session_store; +use crate::mcp_server::ServerRuntime; use async_trait::async_trait; -pub use in_memory::*; +pub use in_memory_session_store::*; use rust_mcp_transport::SessionId; -use tokio::sync::Mutex; - -use crate::mcp_server::ServerRuntime; - -// Type alias for the server-side duplex stream used in sessions -pub type TxServer = Arc; +use std::sync::Arc; /// Trait defining the interface for session storage operations /// @@ -23,25 +17,26 @@ pub trait SessionStore: Send + Sync { /// * `key` - The session identifier to look up /// /// # Returns - /// * `Option>>` - The session stream wrapped in `Arc` if found, None otherwise - async fn get(&self, key: &SessionId) -> Option>>; + /// * `Option>` - The session stream if found, None otherwise + async fn get(&self, key: &SessionId) -> Option>; /// Stores a new session with the given identifier /// /// # Arguments /// * `key` - The session identifier /// * `value` - The duplex stream to store - async fn set(&self, key: SessionId, value: TxServer); + async fn set(&self, key: SessionId, value: Arc); /// Deletes a session by its identifier /// /// # Arguments /// * `key` - The session identifier to delete async fn delete(&self, key: &SessionId); - /// Clears all sessions from the store - async fn clear(&self); + + async fn has(&self, session: &SessionId) -> bool; async fn keys(&self) -> Vec; - async fn values(&self) -> Vec>>; + async fn values(&self) -> Vec>; - async fn has(&self, session: &SessionId) -> bool; + /// Clears all sessions from the store + async fn clear(&self); } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs b/crates/rust-mcp-sdk/src/session_store/in_memory_session_store.rs similarity index 77% rename from crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs rename to crates/rust-mcp-sdk/src/session_store/in_memory_session_store.rs index 80cba3f..169f9a9 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs +++ b/crates/rust-mcp-sdk/src/session_store/in_memory_session_store.rs @@ -1,18 +1,18 @@ +use crate::mcp_server::ServerRuntime; + use super::SessionId; -use super::{SessionStore, TxServer}; +use super::SessionStore; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::Mutex; use tokio::sync::RwLock; /// In-memory session store implementation /// /// Stores session data in a thread-safe HashMap, using a read-write lock for -/// concurrent access and mutexes for individual session streams. #[derive(Clone, Default)] pub struct InMemorySessionStore { - store: Arc>>>>, + store: Arc>>>, } impl InMemorySessionStore { @@ -32,17 +32,16 @@ impl InMemorySessionStore { /// Implementation of the SessionStore trait for InMemorySessionStore /// /// Provides asynchronous methods for managing sessions in memory, ensuring -/// thread-safety through read-write locks and mutexes. #[async_trait] impl SessionStore for InMemorySessionStore { - async fn get(&self, key: &SessionId) -> Option>> { + async fn get(&self, key: &SessionId) -> Option> { let store = self.store.read().await; store.get(key).cloned() } - async fn set(&self, key: SessionId, value: TxServer) { + async fn set(&self, key: SessionId, value: Arc) { let mut store = self.store.write().await; - store.insert(key, Arc::new(Mutex::new(value))); + store.insert(key, value); } async fn delete(&self, key: &SessionId) { @@ -58,7 +57,7 @@ impl SessionStore for InMemorySessionStore { let store = self.store.read().await; store.keys().cloned().collect::>() } - async fn values(&self) -> Vec>> { + async fn values(&self) -> Vec> { let store = self.store.read().await; store.values().cloned().collect::>() } diff --git a/crates/rust-mcp-sdk/src/utils.rs b/crates/rust-mcp-sdk/src/utils.rs index 16fe7c7..2d80f1e 100644 --- a/crates/rust-mcp-sdk/src/utils.rs +++ b/crates/rust-mcp-sdk/src/utils.rs @@ -1,6 +1,5 @@ -use crate::schema::schema_utils::{ClientMessages, SdkError}; - use crate::error::{McpSdkError, ProtocolErrorKind, SdkResult}; +use crate::schema::schema_utils::{ClientMessages, SdkError}; use crate::schema::ProtocolVersion; use std::cmp::Ordering; diff --git a/crates/rust-mcp-transport/CHANGELOG.md b/crates/rust-mcp-transport/CHANGELOG.md index 2d692b4..d3170b9 100644 --- a/crates/rust-mcp-transport/CHANGELOG.md +++ b/crates/rust-mcp-transport/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.6.1](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-transport-v0.6.0...rust-mcp-transport-v0.6.1) (2025-10-13) + + +### πŸš€ Features + +* **server:** Decouple core logic from HTTP server for improved architecture ([#106](https://github.com/rust-mcp-stack/rust-mcp-sdk/issues/106)) ([d10488b](https://github.com/rust-mcp-stack/rust-mcp-sdk/commit/d10488bac739bf28b45d636129eb598d4dd87fd2)) + ## [0.6.0](https://github.com/rust-mcp-stack/rust-mcp-sdk/compare/rust-mcp-transport-v0.5.0...rust-mcp-transport-v0.6.0) (2025-09-19) diff --git a/crates/rust-mcp-transport/Cargo.toml b/crates/rust-mcp-transport/Cargo.toml index 8331eaf..e9605fc 100644 --- a/crates/rust-mcp-transport/Cargo.toml +++ b/crates/rust-mcp-transport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-mcp-transport" -version = "0.6.0" +version = "0.6.1" authors = ["Ali Hashemi"] categories = ["data-structures"] description = "Transport implementations for the MCP (Model Context Protocol) within the rust-mcp-sdk ecosystem, enabling asynchronous data exchange and efficient message handling between MCP clients and servers." diff --git a/crates/rust-mcp-transport/src/lib.rs b/crates/rust-mcp-transport/src/lib.rs index d21e5dd..7566290 100644 --- a/crates/rust-mcp-transport/src/lib.rs +++ b/crates/rust-mcp-transport/src/lib.rs @@ -31,6 +31,9 @@ pub use sse::*; pub use stdio::*; pub use transport::*; +#[cfg(any(feature = "sse", feature = "streamable-http"))] +pub use utils::SseEvent; + // Type alias for session identifier, represented as a String pub type SessionId = String; // Type alias for stream identifier (that will be used at the transport scope), represented as a String diff --git a/crates/rust-mcp-transport/src/utils.rs b/crates/rust-mcp-transport/src/utils.rs index 034f062..36977a2 100644 --- a/crates/rust-mcp-transport/src/utils.rs +++ b/crates/rust-mcp-transport/src/utils.rs @@ -1,40 +1,57 @@ mod cancellation_token; + #[cfg(any(feature = "sse", feature = "streamable-http"))] mod http_utils; + #[cfg(any(feature = "sse", feature = "streamable-http"))] mod readable_channel; + +#[cfg(any(feature = "sse", feature = "streamable-http"))] +mod sse_event; + #[cfg(any(feature = "sse", feature = "streamable-http"))] mod sse_parser; + #[cfg(feature = "sse")] mod sse_stream; + #[cfg(feature = "streamable-http")] mod streamable_http_stream; + +mod time_utils; + #[cfg(any(feature = "sse", feature = "streamable-http"))] mod writable_channel; +use crate::error::{TransportError, TransportResult}; +use crate::schema::schema_utils::SdkError; pub(crate) use cancellation_token::*; + +#[cfg(any(feature = "sse", feature = "streamable-http"))] +use crate::SessionId; + #[cfg(any(feature = "sse", feature = "streamable-http"))] pub(crate) use http_utils::*; + #[cfg(any(feature = "sse", feature = "streamable-http"))] pub(crate) use readable_channel::*; + +#[cfg(any(feature = "sse", feature = "streamable-http"))] +pub use sse_event::*; + #[cfg(any(feature = "sse", feature = "streamable-http"))] pub(crate) use sse_parser::*; + #[cfg(feature = "sse")] pub(crate) use sse_stream::*; + #[cfg(feature = "streamable-http")] pub(crate) use streamable_http_stream::*; -#[cfg(any(feature = "sse", feature = "streamable-http"))] -pub(crate) use writable_channel::*; -mod time_utils; -pub use time_utils::*; -use crate::schema::schema_utils::SdkError; +pub use time_utils::*; use tokio::time::{timeout, Duration}; - -use crate::error::{TransportError, TransportResult}; - #[cfg(any(feature = "sse", feature = "streamable-http"))] -use crate::SessionId; +pub(crate) use writable_channel::*; pub async fn await_timeout(operation: F, timeout_duration: Duration) -> TransportResult where diff --git a/crates/rust-mcp-transport/src/utils/sse_event.rs b/crates/rust-mcp-transport/src/utils/sse_event.rs new file mode 100644 index 0000000..5837807 --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/sse_event.rs @@ -0,0 +1,122 @@ +use bytes::Bytes; +use core::fmt; + +/// Represents a single Server-Sent Event (SSE) as defined in the SSE protocol. +/// +/// Contains the event type, data payload, and optional event ID. +#[derive(Clone, Default)] +pub struct SseEvent { + /// The optional event type (e.g., "message"). + pub event: Option, + /// The optional data payload of the event, stored as bytes. + pub data: Option, + /// The optional event ID for reconnection or tracking purposes. + pub id: Option, + /// Optional reconnection retry interval (in milliseconds). + pub retry: Option, +} + +impl SseEvent { + /// Creates a new `SseEvent` with the given string data. + pub fn new>(data: T) -> Self { + Self { + event: None, + data: Some(Bytes::from(data.into())), + id: None, + retry: None, + } + } + + /// Sets the event name (e.g., "message"). + pub fn with_event>(mut self, event: T) -> Self { + self.event = Some(event.into()); + self + } + + /// Sets the ID of the event. + pub fn with_id>(mut self, id: T) -> Self { + self.id = Some(id.into()); + self + } + + /// Sets the retry interval (in milliseconds). + pub fn with_retry(mut self, retry: u64) -> Self { + self.retry = Some(retry); + self + } + + /// Sets the data as bytes. + pub fn with_data_bytes(mut self, data: Bytes) -> Self { + self.data = Some(data); + self + } + + /// Sets the data. + pub fn with_data(mut self, data: String) -> Self { + self.data = Some(Bytes::from(data)); + self + } + + /// Converts the event into a string in SSE format (ready for HTTP body). + pub fn to_sse_string(&self) -> String { + self.to_string() + } + + pub fn as_bytes(&self) -> Bytes { + Bytes::from(self.to_string()) + } +} + +impl std::fmt::Display for SseEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Emit retry interval + if let Some(retry) = self.retry { + writeln!(f, "retry: {retry}")?; + } + + // Emit ID + if let Some(id) = &self.id { + writeln!(f, "id: {id}")?; + } + + // Emit event type + if let Some(event) = &self.event { + writeln!(f, "event: {event}")?; + } + + // Emit data lines + if let Some(data) = &self.data { + match std::str::from_utf8(data) { + Ok(text) => { + for line in text.lines() { + writeln!(f, "data: {line}")?; + } + } + Err(_) => { + writeln!(f, "data: [binary data]")?; + } + } + } + + writeln!(f)?; // Trailing newline for SSE message end, separates events + Ok(()) + } +} + +impl fmt::Debug for SseEvent { + /// Formats the `SseEvent` for debugging, converting the `data` field to a UTF-8 string + /// (with lossy conversion if invalid UTF-8 is encountered). + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let data_str = self + .data + .as_ref() + .map(|b| String::from_utf8_lossy(b).to_string()); + + f.debug_struct("SseEvent") + .field("event", &self.event) + .field("data", &data_str) + .field("id", &self.id) + .field("retry", &self.retry) + .finish() + } +} diff --git a/crates/rust-mcp-transport/src/utils/sse_parser.rs b/crates/rust-mcp-transport/src/utils/sse_parser.rs index 5933726..3074e9f 100644 --- a/crates/rust-mcp-transport/src/utils/sse_parser.rs +++ b/crates/rust-mcp-transport/src/utils/sse_parser.rs @@ -1,66 +1,9 @@ -use core::fmt; +use bytes::{Bytes, BytesMut}; use std::collections::HashMap; -use bytes::{Bytes, BytesMut}; +use super::SseEvent; const BUFFER_CAPACITY: usize = 1024; -/// Represents a single Server-Sent Event (SSE) as defined in the SSE protocol. -/// -/// Contains the event type, data payload, and optional event ID. -pub struct SseEvent { - /// The optional event type (e.g., "message"). - pub event: Option, - /// The optional data payload of the event, stored as bytes. - pub data: Option, - /// The optional event ID for reconnection or tracking purposes. - pub id: Option, -} - -impl std::fmt::Display for SseEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(id) = &self.id { - writeln!(f, "id: {id}")?; - } - - if let Some(event) = &self.event { - writeln!(f, "event: {event}")?; - } - - if let Some(data) = &self.data { - match std::str::from_utf8(data) { - Ok(text) => { - for line in text.lines() { - writeln!(f, "data: {line}")?; - } - } - Err(_) => { - writeln!(f, "data: [binary data]")?; - } - } - } - - writeln!(f)?; // Trailing newline for SSE message end - Ok(()) - } -} - -impl fmt::Debug for SseEvent { - /// Formats the `SseEvent` for debugging, converting the `data` field to a UTF-8 string - /// (with lossy conversion if invalid UTF-8 is encountered). - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let data_str = self - .data - .as_ref() - .map(|b| String::from_utf8_lossy(b).to_string()); - - f.debug_struct("SseEvent") - .field("event", &self.event) - .field("data", &data_str) - .field("id", &self.id) - .finish() - } -} - /// A parser for Server-Sent Events (SSE) that processes incoming byte chunks into `SseEvent`s. /// This Parser is specifically designed for MCP messages and with no multi-line data support /// @@ -193,11 +136,15 @@ impl SseParser { // Get event (default to None) let event = fields.get("event").cloned(); let id = fields.get("id").cloned(); + let retry = fields + .get("retry") + .and_then(|r| r.trim().parse::().ok()); Some(SseEvent { event, data: Some(data), id, + retry, }) } } @@ -317,4 +264,20 @@ mod tests { Some(Bytes::from("second\n").as_ref()) ); } + + #[test] + fn test_basic_sse_event() { + let mut parser = SseParser::new(); + let input = Bytes::from("event: message\ndata: Hello\nid: 1\nretry: 5000\n\n"); + + let events = parser.process_new_chunk(input); + + assert_eq!(events.len(), 1); + + let event = &events[0]; + assert_eq!(event.event.as_deref(), Some("message")); + assert_eq!(event.data.as_deref(), Some(Bytes::from("Hello\n").as_ref())); + assert_eq!(event.id.as_deref(), Some("1")); + assert_eq!(event.retry, Some(5000)); + } } diff --git a/examples/hello-world-mcp-server-stdio-core/Cargo.toml b/examples/hello-world-mcp-server-stdio-core/Cargo.toml index f37d4c4..5166763 100644 --- a/examples/hello-world-mcp-server-stdio-core/Cargo.toml +++ b/examples/hello-world-mcp-server-stdio-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-world-mcp-server-stdio-core" -version = "0.1.20" +version = "0.1.21" edition = "2021" publish = false license = "MIT" diff --git a/examples/hello-world-mcp-server-stdio/Cargo.toml b/examples/hello-world-mcp-server-stdio/Cargo.toml index 1947dce..397371f 100644 --- a/examples/hello-world-mcp-server-stdio/Cargo.toml +++ b/examples/hello-world-mcp-server-stdio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-world-mcp-server-stdio" -version = "0.1.29" +version = "0.1.30" edition = "2021" publish = false license = "MIT" diff --git a/examples/hello-world-mcp-server-stdio/src/main.rs b/examples/hello-world-mcp-server-stdio/src/main.rs index 98ff6f0..9e5d2b3 100644 --- a/examples/hello-world-mcp-server-stdio/src/main.rs +++ b/examples/hello-world-mcp-server-stdio/src/main.rs @@ -1,19 +1,17 @@ mod handler; mod tools; -use std::sync::Arc; - use handler::MyServerHandler; use rust_mcp_sdk::schema::{ Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, LATEST_PROTOCOL_VERSION, }; - use rust_mcp_sdk::{ error::SdkResult, mcp_server::{server_runtime, ServerRuntime}, McpServer, StdioTransport, TransportOptions, }; +use std::sync::Arc; #[tokio::main] async fn main() -> SdkResult<()> { diff --git a/examples/hello-world-server-streamable-http-core/Cargo.toml b/examples/hello-world-server-streamable-http-core/Cargo.toml index 85e470a..2e1010f 100644 --- a/examples/hello-world-server-streamable-http-core/Cargo.toml +++ b/examples/hello-world-server-streamable-http-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-world-server-streamable-http-core" -version = "0.1.20" +version = "0.1.21" edition = "2021" publish = false license = "MIT" @@ -11,6 +11,7 @@ rust-mcp-sdk = { workspace = true, default-features = false, features = [ "server", "macros", "streamable-http", + "sse", "hyper-server", "2025_06_18", ] } diff --git a/examples/hello-world-server-streamable-http/Cargo.toml b/examples/hello-world-server-streamable-http/Cargo.toml index 61d080f..db5212d 100644 --- a/examples/hello-world-server-streamable-http/Cargo.toml +++ b/examples/hello-world-server-streamable-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-world-server-streamable-http" -version = "0.1.32" +version = "0.1.33" edition = "2021" publish = false license = "MIT" @@ -11,6 +11,7 @@ rust-mcp-sdk = { workspace = true, default-features = false, features = [ "server", "macros", "streamable-http", + "sse", "hyper-server", "2025_06_18", ] } diff --git a/examples/hello-world-server-streamable-http/src/main.rs b/examples/hello-world-server-streamable-http/src/main.rs index 3923a6d..c4fd373 100644 --- a/examples/hello-world-server-streamable-http/src/main.rs +++ b/examples/hello-world-server-streamable-http/src/main.rs @@ -1,19 +1,16 @@ mod handler; mod tools; -use std::sync::Arc; -use std::time::Duration; - +use handler::MyServerHandler; use rust_mcp_sdk::event_store::InMemoryEventStore; use rust_mcp_sdk::mcp_server::{hyper_server, HyperServerOptions}; - -use handler::MyServerHandler; use rust_mcp_sdk::schema::{ Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, LATEST_PROTOCOL_VERSION, }; - use rust_mcp_sdk::{error::SdkResult, mcp_server::ServerHandler}; +use std::sync::Arc; +use std::time::Duration; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub struct AppStateX { diff --git a/examples/simple-mcp-client-sse-core/Cargo.toml b/examples/simple-mcp-client-sse-core/Cargo.toml index 05654fc..46b6790 100644 --- a/examples/simple-mcp-client-sse-core/Cargo.toml +++ b/examples/simple-mcp-client-sse-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-sse-core" -version = "0.1.20" +version = "0.1.21" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-sse/Cargo.toml b/examples/simple-mcp-client-sse/Cargo.toml index 0720afe..a2f4a73 100644 --- a/examples/simple-mcp-client-sse/Cargo.toml +++ b/examples/simple-mcp-client-sse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-sse" -version = "0.1.23" +version = "0.1.24" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-stdio-core/Cargo.toml b/examples/simple-mcp-client-stdio-core/Cargo.toml index f7dc568..2db9211 100644 --- a/examples/simple-mcp-client-stdio-core/Cargo.toml +++ b/examples/simple-mcp-client-stdio-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-stdio-core" -version = "0.1.29" +version = "0.1.30" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-stdio/Cargo.toml b/examples/simple-mcp-client-stdio/Cargo.toml index 7bbd890..9560e88 100644 --- a/examples/simple-mcp-client-stdio/Cargo.toml +++ b/examples/simple-mcp-client-stdio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-stdio" -version = "0.1.29" +version = "0.1.30" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-streamable-http-core/Cargo.toml b/examples/simple-mcp-client-streamable-http-core/Cargo.toml index c8b3464..b53824c 100644 --- a/examples/simple-mcp-client-streamable-http-core/Cargo.toml +++ b/examples/simple-mcp-client-streamable-http-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-streamable-http-core" -version = "0.1.1" +version = "0.1.2" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-streamable-http-core/README.md b/examples/simple-mcp-client-streamable-http-core/README.md index a0852fb..140d7e2 100644 --- a/examples/simple-mcp-client-streamable-http-core/README.md +++ b/examples/simple-mcp-client-streamable-http-core/README.md @@ -1,12 +1,12 @@ -# Simple MCP Client Core (SSE) +# Simple MCP Client Core (Streamable HTTP) -This is a simple MCP (Model Context Protocol) client implemented with the rust-mcp-sdk, dmeonstrating SSE transport, showcasing fundamental MCP client operations like fetching the MCP server's capabilities and executing a tool call. +This is a simple MCP (Model Context Protocol) client implemented with the rust-mcp-sdk, dmeonstrating Streamable HTTP transport, showcasing fundamental MCP client operations like fetching the MCP server's capabilities and executing a tool call. ## Overview This project demonstrates a basic MCP client implementation, showcasing the features of the [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk). -This example connects to a running instance of the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, which has already been started with the sse flag. +This example connects to a running instance of the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, which has already been started with the `streamableHttp` argument. It displays the server name and version, outlines the server's capabilities, and provides a list of available tools, prompts, templates, resources, and more offered by the server. Additionally, it will execute a tool call by utilizing the add tool from the server-everything package to sum two numbers and output the result. @@ -21,20 +21,20 @@ git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git cd rust-mcp-sdk ``` -2- Start `@modelcontextprotocol/server-everything` with SSE argument: +2- Start `@modelcontextprotocol/server-everything` with `streamableHttp` argument: ```bash -npx @modelcontextprotocol/server-everything sse +npx @modelcontextprotocol/server-everything streamableHttp ``` -> It launches the server, making everything accessible via the SSE transport at http://localhost:3001/sse. +> It launches the server, making everything accessible via the streamableHttp transport at http://localhost:3001/mcp. 2. Open a new terminal and run the project with: ```bash -cargo run -p simple-mcp-client-sse-core +cargo run -p simple-mcp-client-streamable-http-core ``` You can observe a sample output of the project; however, your results may vary slightly depending on the version of the MCP Server in use when you run it. - + diff --git a/examples/simple-mcp-client-streamable-http-core/simple-mcp-client-sse.png b/examples/simple-mcp-client-streamable-http-core/simple-mcp-client-sse.png new file mode 100644 index 0000000..37a80fb Binary files /dev/null and b/examples/simple-mcp-client-streamable-http-core/simple-mcp-client-sse.png differ diff --git a/examples/simple-mcp-client-streamable-http/Cargo.toml b/examples/simple-mcp-client-streamable-http/Cargo.toml index bf2827a..3a5fa02 100644 --- a/examples/simple-mcp-client-streamable-http/Cargo.toml +++ b/examples/simple-mcp-client-streamable-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-mcp-client-streamable-http" -version = "0.1.1" +version = "0.1.2" edition = "2021" publish = false license = "MIT" diff --git a/examples/simple-mcp-client-streamable-http/README.md b/examples/simple-mcp-client-streamable-http/README.md index 5b4488e..c876ebb 100644 --- a/examples/simple-mcp-client-streamable-http/README.md +++ b/examples/simple-mcp-client-streamable-http/README.md @@ -1,12 +1,12 @@ -# Simple MCP Client (SSE) +# Simple MCP Client (Streamable HTTP) -This is a simple MCP (Model Context Protocol) client implemented with the rust-mcp-sdk, dmeonstrating SSE transport, showcasing fundamental MCP client operations like fetching the MCP server's capabilities and executing a tool call. +This is a simple MCP (Model Context Protocol) client implemented with the rust-mcp-sdk, dmeonstrating StreamableHTTP transport, showcasing fundamental MCP client operations like fetching the MCP server's capabilities and executing a tool call. ## Overview This project demonstrates a basic MCP client implementation, showcasing the features of the [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk). -This example connects to a running instance of the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, which has already been started with the sse flag. +This example connects to a running instance of the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, which has already been started with the `streamableHttp` argument. It displays the server name and version, outlines the server's capabilities, and provides a list of available tools, prompts, templates, resources, and more offered by the server. Additionally, it will execute a tool call by utilizing the add tool from the server-everything package to sum two numbers and output the result. @@ -21,20 +21,20 @@ git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git cd rust-mcp-sdk ``` -2- Start `@modelcontextprotocol/server-everything` with SSE argument: +2- Start `@modelcontextprotocol/server-everything` with `streamableHttp` argument: ```bash -npx @modelcontextprotocol/server-everything sse +npx @modelcontextprotocol/server-everything streamableHttp ``` -> It launches the server, making everything accessible via the SSE transport at http://localhost:3001/sse. +> It launches the server, making everything accessible via the streamableHttp transport at http://localhost:3001/mcp. 2. Open a new terminal and run the project with: ```bash -cargo run -p simple-mcp-client-sse +cargo run -p simple-mcp-client-streamable-http ``` You can observe a sample output of the project; however, your results may vary slightly depending on the version of the MCP Server in use when you run it. - +