diff --git a/Cargo.lock b/Cargo.lock index 66a51f0..bd71ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,48 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -28,17 +70,122 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -46,7 +193,48 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", ] [[package]] @@ -55,18 +243,64 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cc" +version = "1.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "colored" version = "3.0.0" @@ -76,6 +310,171 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -124,6 +523,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -147,6 +561,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -166,391 +586,1949 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hello-world-mcp-server" -version = "0.1.9" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "async-trait", - "futures", - "rust-mcp-schema", - "rust-mcp-sdk", - "serde", - "serde_json", - "tokio", + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] -name = "hello-world-mcp-server-core" -version = "0.1.9" +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "async-trait", - "futures", - "rust-mcp-schema", - "rust-mcp-sdk", - "serde", - "serde_json", - "tokio", + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] [[package]] -name = "libc" -version = "0.2.171" +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "hello-world-mcp-server" +version = "0.1.9" +dependencies = [ + "async-trait", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "hello-world-mcp-server-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "hello-world-server-core-sse" +version = "0.1.0" +dependencies = [ + "async-trait", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "hello-world-server-sse" +version = "0.1.9" +dependencies = [ + "async-trait", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.10", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +dependencies = [ + "cfg-if", + "windows-targets 0.53.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.10", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-mcp-macros" +version = "0.2.1" +dependencies = [ + "proc-macro2", + "quote", + "rust-mcp-schema", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "rust-mcp-schema" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868d31d0ae0376ba45786eac9058771da06839e83bb961ac7e5997ab3910f086" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "rust-mcp-sdk" +version = "0.2.4" +dependencies = [ + "async-trait", + "axum", + "axum-server", + "futures", + "hyper 1.6.0", + "rust-mcp-macros", + "rust-mcp-schema", + "rust-mcp-transport", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "rust-mcp-transport" +version = "0.2.1" +dependencies = [ + "async-trait", + "axum", + "bytes", + "futures", + "reqwest", + "rust-mcp-schema", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "wiremock", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simple-mcp-client" +version = "0.1.9" +dependencies = [ + "async-trait", + "colored", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "simple-mcp-client-core" +version = "0.1.9" +dependencies = [ + "async-trait", + "colored", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "simple-mcp-client-core-sse" +version = "0.1.0" +dependencies = [ + "async-trait", + "colored", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", +] [[package]] -name = "lock_api" -version = "0.4.12" +name = "simple-mcp-client-sse" +version = "0.1.0" +dependencies = [ + "async-trait", + "colored", + "futures", + "rust-mcp-schema", + "rust-mcp-sdk", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", - "scopeguard", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "smallvec" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] -name = "miniz_oxide" -version = "0.8.7" +name = "socket2" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ - "adler2", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "mio" -version = "1.0.3" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ + "core-foundation-sys", "libc", - "wasi", - "windows-sys 0.52.0", ] [[package]] -name = "object" -version = "0.36.7" +name = "tempfile" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "memchr", + "fastrand 2.3.0", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", ] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "lock_api", - "parking_lot_core", + "thiserror-impl 1.0.69", ] [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "thiserror" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", "libc", - "redox_syscall", - "smallvec", - "windows-targets", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "tokio-macros" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] [[package]] -name = "proc-macro2" -version = "1.0.94" +name = "tokio-rustls" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "unicode-ident", + "rustls", + "tokio", ] [[package]] -name = "quote" -version = "1.0.40" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ - "proc-macro2", + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] -name = "redox_syscall" -version = "0.5.11" +name = "tokio-util" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ - "bitflags", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] -name = "rust-mcp-macros" -version = "0.2.1" +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "rust-mcp-schema", - "serde", - "serde_json", "syn", ] [[package]] -name = "rust-mcp-schema" -version = "0.4.0" +name = "tracing-core" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868d31d0ae0376ba45786eac9058771da06839e83bb961ac7e5997ab3910f086" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ - "serde", - "serde_json", + "once_cell", + "valuable", ] [[package]] -name = "rust-mcp-sdk" -version = "0.2.4" +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "async-trait", - "futures", - "rust-mcp-macros", - "rust-mcp-schema", - "rust-mcp-transport", + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", "serde", - "serde_json", - "thiserror", - "tokio", ] [[package]] -name = "rust-mcp-transport" -version = "0.2.1" +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "async-trait", - "futures", - "rust-mcp-schema", - "serde", - "serde_json", - "thiserror", - "tokio", + "try-lock", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] -name = "ryu" -version = "1.0.20" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "scopeguard" -version = "1.2.0" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] -name = "serde" -version = "1.0.219" +name = "wasm-bindgen" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "serde_derive", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "wasm-bindgen-backend" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ + "bumpalo", + "log", "proc-macro2", "quote", "syn", + "wasm-bindgen-shared", ] [[package]] -name = "serde_json" -version = "1.0.140" +name = "wasm-bindgen-futures" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "signal-hook-registry" -version = "1.4.2" +name = "wasm-bindgen-macro" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "libc", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "simple-mcp-client" -version = "0.1.9" +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "async-trait", - "colored", - "futures", - "rust-mcp-schema", - "rust-mcp-sdk", - "serde", - "serde_json", - "thiserror", - "tokio", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] -name = "simple-mcp-client-core" -version = "0.1.9" +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ - "async-trait", - "colored", - "futures", - "rust-mcp-schema", - "rust-mcp-sdk", - "serde", - "serde_json", - "thiserror", - "tokio", + "unicode-ident", ] [[package]] -name = "slab" -version = "0.4.9" +name = "wasm-streams" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ - "autocfg", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "smallvec" -version = "1.15.0" +name = "web-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "socket2" -version = "0.5.9" +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ - "libc", - "windows-sys 0.52.0", + "either", + "home", + "once_cell", + "rustix 0.38.44", ] [[package]] -name = "syn" -version = "2.0.100" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "thiserror" -version = "2.0.12" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "thiserror-impl" -version = "2.0.12" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "tokio" -version = "1.44.2" +name = "windows-link" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] -name = "tokio-macros" -version = "2.5.0" +name = "windows-registry" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-result", + "windows-strings", + "windows-targets 0.53.0", ] [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "windows-result" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +dependencies = [ + "windows-link", +] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "windows-strings" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -558,7 +2536,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -567,7 +2545,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -576,14 +2554,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "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", ] [[package]] @@ -592,44 +2586,233 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.7", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper 0.14.32", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 09eb82f..d19b489 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ members = [ "examples/simple-mcp-client-core", "examples/hello-world-mcp-server", "examples/hello-world-mcp-server-core", + "examples/hello-world-server-sse", + "examples/hello-world-server-core-sse", + "examples/simple-mcp-client-sse", + "examples/simple-mcp-client-core-sse", ] [workspace.dependencies] @@ -26,6 +30,7 @@ async-trait = { version = "0.1" } strum = { version = "0.27", features = ["derive"] } thiserror = { version = "2.0" } tokio-stream = { version = "0.1" } +uuid = { version = "1" } tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "env-filter", @@ -33,6 +38,12 @@ tracing-subscriber = { version = "0.3", features = [ "fmt", ] } +axum = "0.8" +rustls = "0.23" +tokio-rustls = "0.26" +axum-server = { version = "0.7" } +reqwest = "0.12" +bytes = "1.10.1" # [workspace.dependencies.windows] diff --git a/Makefile.toml b/Makefile.toml index f8b65af..8d11b2d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -8,7 +8,7 @@ args = ["fmt", "--all", "--", "--check"] [tasks.clippy] command = "cargo" -args = ["clippy"] +args = ["clippy", "--workspace", "--all-targets", "--all-features"] [tasks.test] install_crate = "nextest" diff --git a/README.md b/README.md index 3034fdf..b79bd66 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,44 @@ [Hello World MCP Server ](examples/hello-world-mcp-server) -A high-performance, asynchronous toolkit for building MCP servers and clients. +A high-performance, asynchronous toolkit for building MCP servers and clients. Focus on your app's logic while **rust-mcp-sdk** takes care of the rest! -**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem. +**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem. Leveraging the [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) crate simplifies the process of building robust and reliable MCP servers and clients, ensuring consistency and minimizing errors in data handling and message processing. -**⚠️WARNING**: This project only supports Standard Input/Output (stdio) transport at this time, with support for SSE (Server-Sent Events) transport still in progress and not yet available. Project is currently under development and should be used at your own risk. +This project currently supports following transports: +- **stdio** (Standard Input/Output) +- **sse** (Server-Sent Events). -## Projects using `rust-mcp-sdk` -Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their name, description, and links to their repositories or project pages. -| Icon | Name | Description | Link | -|------|------|-------------|------| -| | [Rust MCP Filesystem](https://rust-mcp-stack.github.io/rust-mcp-filesystem) | Fast, asynchronous MCP server for seamless filesystem operations offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. | [GitHub](https://github.com/rust-mcp-stack/rust-mcp-filesystem) | -| | [MCP Discovery](https://rust-mcp-stack.github.io/mcp-discovery) | A lightweight command-line tool for discovering and documenting MCP Server capabilities. | [GitHub](https://github.com/rust-mcp-stack/mcp-discovery) | +🚀 The **rust-mcp-sdk** includes a lightweight [Axum](https://github.com/tokio-rs/axum) based server that handles all core functionality seamlessly. Switching between `stdio` and `sse` is straightforward, requiring minimal code changes. The server is designed to efficiently handle multiple concurrent client connections and offers built-in support for SSL. + +**⚠️** **Streamable HTTP** transport and authentication still in progress and not yet available. Project is currently under development and should be used at your own risk. + +## Table of Contents +- [Usage Examples](#usage-examples) + - [MCP Server (stdio)](#mcp-server-stdio) + - [MCP Server (sse)](#mcp-server-sse) + - [MCP Client (stdio)](#mcp-client-stdio) + - [MCP Client (sse)](#mcp-client-sse) +- [Cargo features](#cargo-features) + - [Available Features](#available-features) + - [Default Features](#default-features) + - [Using Only the server Features](#using-only-the-server-features) + - [Using Only the client Features](#using-only-the-client-features) +- [Choosing Between Standard and Core Handlers traits](#choosing-between-standard-and-core-handlers-traits) + - [Choosing Between **ServerHandler** and **ServerHandlerCore**](#choosing-between-serverhandler-and-serverhandlercore) + - [Choosing Between **ClientHandler** and **ClientHandlerCore**](#choosing-between-clienthandler-and-clienthandlercore) +- [Projects using Rust MCP SDK](#projects-using-rust-mcp-sdk) +- [Contributing](#contributing) +- [Development](#development) +- [License](#license) ## Usage Examples -### MCP Server +### MCP Server (stdio) Create a MCP server with a `tool` that will print a `Hello World!` message: @@ -70,7 +88,55 @@ async fn main() -> SdkResult<()> { } ``` -The implementation of `MyServerHandler` could be as simple as the following: +See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : + +![mcp-server in rust](assets/examples/hello-world-mcp-server.gif) + +### MCP Server (sse) + +Creating an MCP server in `rust-mcp-sdk` with the `sse` transport allows multiple clients to connect simultaneously with no additional setup. +Simply create a Hyper Server using `hyper_server::create_server()` and pass in the same handler and transform options. + +```rust + +// STEP 1: Define server details and capabilities +let server_details = InitializeResult { + // server name and version + server_info: Implementation { + name: "Hello World MCP Server".to_string(), + version: "0.1.0".to_string(), + }, + capabilities: ServerCapabilities { + // indicates that server support mcp tools + tools: Some(ServerCapabilitiesTools { list_changed: None }), + ..Default::default() // Using default values for other fields + }, + meta: None, + instructions: Some("server instructions...".to_string()), + protocol_version: LATEST_PROTOCOL_VERSION.to_string(), +}; + +// STEP 2: instantiate our custom handler for handling MCP messages +let handler = MyServerHandler {}; + +// STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions +let server = hyper_server::create_server( + server_details, + handler, + HyperServerOptions { + host: "127.0.0.1".to_string(), + ..Default::default() + }, +); + +// STEP 4: Start the server +server.start().await?; + +Ok(()) +``` + + +The implementation of `MyServerHandler` is the same regardless of the transport used and could be as simple as the following: ```rust @@ -116,13 +182,13 @@ impl ServerHandler for MyServerHandler { 👉 For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)** -See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : +See hello-world-server-sse example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : -![mcp-server in rust](assets/examples/hello-world-mcp-server.gif) +![mcp-server in rust](assets/examples/hello-world-server-sse.gif) --- -### MCP Client +### MCP Client (stdio) Create an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools, then uses the add tool provided by the server to sum 120 and 28, printing the result. @@ -203,23 +269,38 @@ Here is the output : > your results may vary slightly depending on the version of the MCP Server in use when you run it. +### MCP Client (sse) +Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical, with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same: + +```diff +- let transport = StdioTransport::create_with_server_launch( +- "npx", +- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()], +- None, TransportOptions::default() +-)?; ++ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?; +``` + + ## Getting Started If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md) -## Features +## Cargo Features -The `rust-mcp-sdk` crate provides three optional features: `server` , `client` and `macros`. By default, all features are enabled for maximum functionality. You can customize which features to include based on your project's needs. +The `rust-mcp-sdk` crate provides several features that can be enabled or disabled. By default, all features are enabled to ensure maximum functionality, but you can customize which ones to include based on your project's requirements. ### Available Features -- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP services. +- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP servers. - `client`: Activates MCP client capabilities, offering modules and APIs for client development and communicating with MCP servers. +- `hyper-server`: This feature enables the **sse** transport for MCP servers, supporting multiple simultaneous client connections out of the box. +- `ssl`: This feature enables TLS/SSL support for the **sse** transport when used with the `hyper-server`. - `macros`: Provides procedural macros for simplifying the creation and manipulation of MCP Tool structures. -### Default Behavior +### Default Features -All features (server, client, and macros) are enabled by default. When you include rust-mcp-sdk as a dependency without specifying features, all will be included: +All features are enabled by default. When you include rust-mcp-sdk as a dependency without specifying features, all will be included: @@ -230,7 +311,7 @@ rust-mcp-sdk = "0.2.0" -### Using Only the server Feature +### Using Only the server Features If you only need the MCP Server functionality, you can disable the default features and explicitly enable the server feature. Add the following to your Cargo.toml: @@ -240,10 +321,11 @@ If you only need the MCP Server functionality, you can disable the default featu [dependencies] rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros"] } ``` +Optionally add `hyper-server` for **sse** transport, and `ssl` feature for tls/ssl support of the `hyper-server` -### Using Only the client Feature +### Using Only the client Features If you only need the MCP Client functionality, you can disable the default features and explicitly enable the client feature. Add the following to your Cargo.toml: @@ -256,28 +338,59 @@ rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["clien -### Choosing Between `mcp_server_handler` and `mcp_server_handler_core` +## Choosing Between Standard and Core Handlers traits +Learn when to use the `mcp_*_handler` traits versus the lower-level `mcp_*_handler_core` traits for both server and client implementations. This section helps you decide based on your project's need for simplicity versus fine-grained control. + +### Choosing Between `ServerHandler` and `ServerHandlerCore` [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) provides two type of handler traits that you can chose from: -- **mcp_server_handler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs. +- **ServerHandler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs. Refer to [examples/hello-world-mcp-server/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server/src/handler.rs) for an example. -- **mcp_server_handler_core**: If you need more control over MCP messages, consider using `mcp_server_handler_core`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters. +- **ServerHandlerCore**: If you need more control over MCP messages, consider using `ServerHandlerCore`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters. Refer to [examples/hello-world-mcp-server-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-core/src/handler.rs) for an example. --- -**👉 Note:** Depending on your choice between `mcp_server_handler` and `mcp_server_handler_core`, you must use either `server_runtime::create_server()` or `server_runtime_core::create_server()` , respectively. +**👉 Note:** Depending on whether you choose `ServerHandler` or `ServerHandlerCore`, you must use the `create_server()` function from the appropriate module: + +- For `ServerHandler`: + - Use `server_runtime::create_server()` for servers with stdio transport + - Use `hyper_server::create_server()` for servers with sse transport + +- For `ServerHandlerCore`: + - Use `server_runtime_core::create_server()` for servers with stdio transport + - Use `hyper_server_core::create_server()` for servers with sse transport --- -### Choosing Between `mcp_client_handler` and `mcp_client_handler_core` -The same principles outlined above apply to the client-side handlers, `mcp_client_handler` and `mcp_client_handler_core`. -Use `client_runtime::create_client()` or `client_runtime_core::create_client()` , respectively. +### Choosing Between `ClientHandler` and `ClientHandlerCore` + +The same principles outlined above apply to the client-side handlers, `ClientHandler` and `ClientHandlerCore`. + +- Use `client_runtime::create_client()` when working with `ClientHandler` + +- Use `client_runtime_core::create_client()` when working with `ClientHandlerCore` + +Both functions create an MCP client instance. + + + Check out the corresponding examples at: [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) and [examples/simple-mcp-client-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-core). + +## Projects using Rust MCP SDK + +Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their name, description, and links to their repositories or project pages. + +| Icon | Name | Description | Link | +|------|------|-------------|------| +| | [Rust MCP Filesystem](https://rust-mcp-stack.github.io/rust-mcp-filesystem) | Fast, asynchronous MCP server for seamless filesystem operations offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. | [GitHub](https://github.com/rust-mcp-stack/rust-mcp-filesystem) | +| | [MCP Discovery](https://rust-mcp-stack.github.io/mcp-discovery) | A lightweight command-line tool for discovering and documenting MCP Server capabilities. | [GitHub](https://github.com/rust-mcp-stack/mcp-discovery) | + + ## Contributing We welcome everyone who wishes to contribute! Please refer to the [contributing](CONTRIBUTING.md) guidelines for more details. diff --git a/assets/examples/hello-world-server-sse.gif b/assets/examples/hello-world-server-sse.gif new file mode 100644 index 0000000..87de711 Binary files /dev/null and b/assets/examples/hello-world-server-sse.gif differ diff --git a/assets/examples/simple-mcp-client-sse.png b/assets/examples/simple-mcp-client-sse.png new file mode 100644 index 0000000..37a80fb Binary files /dev/null and b/assets/examples/simple-mcp-client-sse.png differ diff --git a/crates/rust-mcp-sdk/Cargo.toml b/crates/rust-mcp-sdk/Cargo.toml index 98d10da..ab5c142 100644 --- a/crates/rust-mcp-sdk/Cargo.toml +++ b/crates/rust-mcp-sdk/Cargo.toml @@ -22,12 +22,35 @@ async-trait = { workspace = true } futures = { workspace = true } thiserror = { workspace = true } +axum = { workspace = true, optional = true } +uuid = { workspace = true, features = ["v4"], optional = true } +tokio-stream = { workspace = true, optional = true } +axum-server = { version = "0.7", features = [], optional = true } +tracing.workspace = true + +# rustls = { workspace = true, optional = true } +hyper = { version = "1.6.0" } + +[dev-dependencies] +tracing-subscriber = { workspace = true, features = [ + "env-filter", + "std", + "fmt", +] } + [features] -default = ["client", "server", "macros"] # All features enabled by default -server = [] # Server feature -client = [] # Client feature +default = [ + "client", + "server", + "macros", + "hyper-server", + "ssl", +] # All features enabled by default +server = [] # Server feature +client = [] # Client feature +hyper-server = ["axum", "axum-server", "uuid", "tokio-stream"] +ssl = ["axum-server/tls-rustls"] macros = ["rust-mcp-macros"] - [lints] workspace = true diff --git a/crates/rust-mcp-sdk/README.md b/crates/rust-mcp-sdk/README.md index 119f2a2..b79bd66 100644 --- a/crates/rust-mcp-sdk/README.md +++ b/crates/rust-mcp-sdk/README.md @@ -9,29 +9,46 @@ [build status ](https://github.com/rust-mcp-stack/rust-mcp-sdk/actions/workflows/ci.yml) [Hello World MCP Server -](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server#hello-world-mcp-server) +](examples/hello-world-mcp-server) - -A high-performance, asynchronous toolkit for building MCP servers and clients. +A high-performance, asynchronous toolkit for building MCP servers and clients. Focus on your app's logic while **rust-mcp-sdk** takes care of the rest! -**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem. +**rust-mcp-sdk** provides the necessary components for developing both servers and clients in the MCP ecosystem. Leveraging the [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) crate simplifies the process of building robust and reliable MCP servers and clients, ensuring consistency and minimizing errors in data handling and message processing. -**⚠️WARNING**: This project only supports Standard Input/Output (stdio) transport at this time, with support for SSE (Server-Sent Events) transport still in progress and not yet available. Project is currently under development and should be used at your own risk. +This project currently supports following transports: +- **stdio** (Standard Input/Output) +- **sse** (Server-Sent Events). -## Projects using `rust-mcp-sdk` -Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their name, description, and links to their repositories or project pages. -| Icon | Name | Description | Link | -|------|------|-------------|------| -| | [Rust MCP Filesystem](https://rust-mcp-stack.github.io/rust-mcp-filesystem) | Fast, asynchronous MCP server for seamless filesystem operations offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. | [GitHub](https://github.com/rust-mcp-stack/rust-mcp-filesystem) | -| | [MCP Discovery](https://rust-mcp-stack.github.io/mcp-discovery) | A lightweight command-line tool for discovering and documenting MCP Server capabilities. | [GitHub](https://github.com/rust-mcp-stack/mcp-discovery) | +🚀 The **rust-mcp-sdk** includes a lightweight [Axum](https://github.com/tokio-rs/axum) based server that handles all core functionality seamlessly. Switching between `stdio` and `sse` is straightforward, requiring minimal code changes. The server is designed to efficiently handle multiple concurrent client connections and offers built-in support for SSL. + +**⚠️** **Streamable HTTP** transport and authentication still in progress and not yet available. Project is currently under development and should be used at your own risk. + +## Table of Contents +- [Usage Examples](#usage-examples) + - [MCP Server (stdio)](#mcp-server-stdio) + - [MCP Server (sse)](#mcp-server-sse) + - [MCP Client (stdio)](#mcp-client-stdio) + - [MCP Client (sse)](#mcp-client-sse) +- [Cargo features](#cargo-features) + - [Available Features](#available-features) + - [Default Features](#default-features) + - [Using Only the server Features](#using-only-the-server-features) + - [Using Only the client Features](#using-only-the-client-features) +- [Choosing Between Standard and Core Handlers traits](#choosing-between-standard-and-core-handlers-traits) + - [Choosing Between **ServerHandler** and **ServerHandlerCore**](#choosing-between-serverhandler-and-serverhandlercore) + - [Choosing Between **ClientHandler** and **ClientHandlerCore**](#choosing-between-clienthandler-and-clienthandlercore) +- [Projects using Rust MCP SDK](#projects-using-rust-mcp-sdk) +- [Contributing](#contributing) +- [Development](#development) +- [License](#license) ## Usage Examples -### MCP Server +### MCP Server (stdio) Create a MCP server with a `tool` that will print a `Hello World!` message: @@ -71,7 +88,55 @@ async fn main() -> SdkResult<()> { } ``` -The implementation of `MyServerHandler` could be as simple as the following: +See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : + +![mcp-server in rust](assets/examples/hello-world-mcp-server.gif) + +### MCP Server (sse) + +Creating an MCP server in `rust-mcp-sdk` with the `sse` transport allows multiple clients to connect simultaneously with no additional setup. +Simply create a Hyper Server using `hyper_server::create_server()` and pass in the same handler and transform options. + +```rust + +// STEP 1: Define server details and capabilities +let server_details = InitializeResult { + // server name and version + server_info: Implementation { + name: "Hello World MCP Server".to_string(), + version: "0.1.0".to_string(), + }, + capabilities: ServerCapabilities { + // indicates that server support mcp tools + tools: Some(ServerCapabilitiesTools { list_changed: None }), + ..Default::default() // Using default values for other fields + }, + meta: None, + instructions: Some("server instructions...".to_string()), + protocol_version: LATEST_PROTOCOL_VERSION.to_string(), +}; + +// STEP 2: instantiate our custom handler for handling MCP messages +let handler = MyServerHandler {}; + +// STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions +let server = hyper_server::create_server( + server_details, + handler, + HyperServerOptions { + host: "127.0.0.1".to_string(), + ..Default::default() + }, +); + +// STEP 4: Start the server +server.start().await?; + +Ok(()) +``` + + +The implementation of `MyServerHandler` is the same regardless of the transport used and could be as simple as the following: ```rust @@ -117,13 +182,13 @@ impl ServerHandler for MyServerHandler { 👉 For a more detailed example of a [Hello World MCP](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server) Server that supports multiple tools and provides more type-safe handling of `CallToolRequest`, check out: **[examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server)** -See hello-world-mcp-server example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : +See hello-world-server-sse example running in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) : -![mcp-server in rust](assets/examples/hello-world-mcp-server.gif) +![mcp-server in rust](assets/examples/hello-world-server-sse.gif) --- -### MCP Client +### MCP Client (stdio) Create an MCP client that starts the [@modelcontextprotocol/server-everything](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) server, displays the server's name, version, and list of tools, then uses the add tool provided by the server to sum 120 and 28, printing the result. @@ -204,23 +269,38 @@ Here is the output : > your results may vary slightly depending on the version of the MCP Server in use when you run it. +### MCP Client (sse) +Creating an MCP client using the `rust-mcp-sdk` with the SSE transport is almost identical, with one exception at `step 3`. Instead of creating a `StdioTransport`, you simply create a `ClientSseTransport`. The rest of the code remains the same: + +```diff +- let transport = StdioTransport::create_with_server_launch( +- "npx", +- vec![ "-y".to_string(), "@modelcontextprotocol/server-everything".to_string()], +- None, TransportOptions::default() +-)?; ++ let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?; +``` + + ## Getting Started If you are looking for a step-by-step tutorial on how to get started with `rust-mcp-sdk` , please see : [Getting Started MCP Server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/doc/getting-started-mcp-server.md) -## Features +## Cargo Features -The `rust-mcp-sdk` crate provides three optional features: `server` , `client` and `macros`. By default, all features are enabled for maximum functionality. You can customize which features to include based on your project's needs. +The `rust-mcp-sdk` crate provides several features that can be enabled or disabled. By default, all features are enabled to ensure maximum functionality, but you can customize which ones to include based on your project's requirements. ### Available Features -- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP services. +- `server`: Activates MCP server capabilities in `rust-mcp-sdk`, providing modules and APIs for building and managing MCP servers. - `client`: Activates MCP client capabilities, offering modules and APIs for client development and communicating with MCP servers. +- `hyper-server`: This feature enables the **sse** transport for MCP servers, supporting multiple simultaneous client connections out of the box. +- `ssl`: This feature enables TLS/SSL support for the **sse** transport when used with the `hyper-server`. - `macros`: Provides procedural macros for simplifying the creation and manipulation of MCP Tool structures. -### Default Behavior +### Default Features -All features (server, client, and macros) are enabled by default. When you include rust-mcp-sdk as a dependency without specifying features, all will be included: +All features are enabled by default. When you include rust-mcp-sdk as a dependency without specifying features, all will be included: @@ -231,7 +311,7 @@ rust-mcp-sdk = "0.2.0" -### Using Only the server Feature +### Using Only the server Features If you only need the MCP Server functionality, you can disable the default features and explicitly enable the server feature. Add the following to your Cargo.toml: @@ -241,10 +321,11 @@ If you only need the MCP Server functionality, you can disable the default featu [dependencies] rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["server","macros"] } ``` +Optionally add `hyper-server` for **sse** transport, and `ssl` feature for tls/ssl support of the `hyper-server` -### Using Only the client Feature +### Using Only the client Features If you only need the MCP Client functionality, you can disable the default features and explicitly enable the client feature. Add the following to your Cargo.toml: @@ -257,28 +338,59 @@ rust-mcp-sdk = { version = "0.2.0", default-features = false, features = ["clien -### Choosing Between `mcp_server_handler` and `mcp_server_handler_core` +## Choosing Between Standard and Core Handlers traits +Learn when to use the `mcp_*_handler` traits versus the lower-level `mcp_*_handler_core` traits for both server and client implementations. This section helps you decide based on your project's need for simplicity versus fine-grained control. + +### Choosing Between `ServerHandler` and `ServerHandlerCore` [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) provides two type of handler traits that you can chose from: -- **mcp_server_handler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs. +- **ServerHandler**: This is the recommended trait for your MCP project, offering a default implementation for all types of MCP messages. It includes predefined implementations within the trait, such as handling initialization or responding to ping requests, so you only need to override and customize the handler functions relevant to your specific needs. Refer to [examples/hello-world-mcp-server/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server/src/handler.rs) for an example. -- **mcp_server_handler_core**: If you need more control over MCP messages, consider using `mcp_server_handler_core`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters. +- **ServerHandlerCore**: If you need more control over MCP messages, consider using `ServerHandlerCore`. It offers three primary methods to manage the three MCP message types: `request`, `notification`, and `error`. While still providing type-safe objects in these methods, it allows you to determine how to handle each message based on its type and parameters. Refer to [examples/hello-world-mcp-server-core/src/handler.rs](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-core/src/handler.rs) for an example. --- -**👉 Note:** Depending on your choice between `mcp_server_handler` and `mcp_server_handler_core`, you must use either `server_runtime::create_server()` or `server_runtime_core::create_server()` , respectively. +**👉 Note:** Depending on whether you choose `ServerHandler` or `ServerHandlerCore`, you must use the `create_server()` function from the appropriate module: + +- For `ServerHandler`: + - Use `server_runtime::create_server()` for servers with stdio transport + - Use `hyper_server::create_server()` for servers with sse transport + +- For `ServerHandlerCore`: + - Use `server_runtime_core::create_server()` for servers with stdio transport + - Use `hyper_server_core::create_server()` for servers with sse transport --- -### Choosing Between `mcp_client_handler` and `mcp_client_handler_core` -The same principles outlined above apply to the client-side handlers, `mcp_client_handler` and `mcp_client_handler_core`. -Use `client_runtime::create_client()` or `client_runtime_core::create_client()` , respectively. +### Choosing Between `ClientHandler` and `ClientHandlerCore` + +The same principles outlined above apply to the client-side handlers, `ClientHandler` and `ClientHandlerCore`. + +- Use `client_runtime::create_client()` when working with `ClientHandler` + +- Use `client_runtime_core::create_client()` when working with `ClientHandlerCore` + +Both functions create an MCP client instance. + + + Check out the corresponding examples at: [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) and [examples/simple-mcp-client-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-core). + +## Projects using Rust MCP SDK + +Below is a list of projects that utilize the `rust-mcp-sdk`, showcasing their name, description, and links to their repositories or project pages. + +| Icon | Name | Description | Link | +|------|------|-------------|------| +| | [Rust MCP Filesystem](https://rust-mcp-stack.github.io/rust-mcp-filesystem) | Fast, asynchronous MCP server for seamless filesystem operations offering enhanced capabilities, improved performance, and a robust feature set tailored for modern filesystem interactions. | [GitHub](https://github.com/rust-mcp-stack/rust-mcp-filesystem) | +| | [MCP Discovery](https://rust-mcp-stack.github.io/mcp-discovery) | A lightweight command-line tool for discovering and documenting MCP Server capabilities. | [GitHub](https://github.com/rust-mcp-stack/mcp-discovery) | + + ## Contributing We welcome everyone who wishes to contribute! Please refer to the [contributing](CONTRIBUTING.md) guidelines for more details. diff --git a/crates/rust-mcp-sdk/assets/examples/hello-world-server-sse.gif b/crates/rust-mcp-sdk/assets/examples/hello-world-server-sse.gif new file mode 100644 index 0000000..87de711 Binary files /dev/null and b/crates/rust-mcp-sdk/assets/examples/hello-world-server-sse.gif differ diff --git a/crates/rust-mcp-sdk/assets/examples/simple-mcp-client-sse.png b/crates/rust-mcp-sdk/assets/examples/simple-mcp-client-sse.png new file mode 100644 index 0000000..37a80fb Binary files /dev/null and b/crates/rust-mcp-sdk/assets/examples/simple-mcp-client-sse.png differ diff --git a/crates/rust-mcp-sdk/src/error.rs b/crates/rust-mcp-sdk/src/error.rs index c6ca132..3b2f00d 100644 --- a/crates/rust-mcp-sdk/src/error.rs +++ b/crates/rust-mcp-sdk/src/error.rs @@ -2,6 +2,9 @@ use rust_mcp_schema::RpcError; use rust_mcp_transport::error::TransportError; use thiserror::Error; +#[cfg(feature = "hyper-server")] +use crate::hyper_servers::error::TransportServerError; + pub type SdkResult = core::result::Result; #[derive(Debug, Error)] @@ -18,6 +21,9 @@ pub enum McpSdkError { AnyError(Box<(dyn std::error::Error + Send + Sync)>), #[error("{0}")] SdkError(#[from] rust_mcp_schema::schema_utils::SdkError), + #[cfg(feature = "hyper-server")] + #[error("{0}")] + TransportServerError(#[from] TransportServerError), } #[deprecated(since = "0.2.0", note = "Use `McpSdkError` instead.")] diff --git a/crates/rust-mcp-sdk/src/hyper_servers.rs b/crates/rust-mcp-sdk/src/hyper_servers.rs new file mode 100644 index 0000000..ad1e2cd --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers.rs @@ -0,0 +1,10 @@ +mod app_state; +pub mod error; +pub mod hyper_server; +pub mod hyper_server_core; +mod routes; +mod server; +mod session_store; + +pub use server::*; +pub use session_store::*; diff --git a/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs b/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs new file mode 100644 index 0000000..3276802 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/app_state.rs @@ -0,0 +1,22 @@ +use std::{sync::Arc, time::Duration}; + +use rust_mcp_schema::InitializeResult; +use rust_mcp_transport::TransportOptions; + +use crate::mcp_traits::mcp_handler::McpServerHandler; + +use super::{session_store::SessionStore, IdGenerator}; + +/// Application state struct for the Hyper server +/// +/// Holds shared, thread-safe references to session storage, ID generator, +/// server details, handler, ping interval, and transport options. +#[derive(Clone)] +pub struct AppState { + pub session_store: Arc, + pub id_generator: Arc, + pub server_details: Arc, + pub handler: Arc, + pub ping_interval: Duration, + pub transport_options: Arc, +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/error.rs b/crates/rust-mcp-sdk/src/hyper_servers/error.rs new file mode 100644 index 0000000..adcccf4 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/error.rs @@ -0,0 +1,33 @@ +use std::net::AddrParseError; + +use axum::{http::StatusCode, response::IntoResponse}; +use thiserror::Error; + +pub type TransportServerResult = core::result::Result; + +#[derive(Debug, Error, Clone)] +pub enum TransportServerError { + #[error("'sessionId' query string is missing!")] + SessionIdMissing, + #[error("No session found for the given ID: {0}.")] + SessionIdInvalid(String), + #[error("Stream IO Error: {0}.")] + StreamIoError(String), + #[error("{0}")] + AddrParseError(#[from] AddrParseError), + #[error("Server start error: {0}")] + ServerStartError(String), + #[error("Invalid options: {0}")] + InvalidServerOptions(String), + #[error("{0}")] + SslCertError(String), +} + +impl IntoResponse for TransportServerError { + //consume self and returns a Response + fn into_response(self) -> axum::response::Response { + let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response(); + response.extensions_mut().insert(self); + response + } +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs new file mode 100644 index 0000000..eb88feb --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use rust_mcp_schema::InitializeResult; + +use crate::mcp_server::{server_runtime::ServerRuntimeInternalHandler, ServerHandler}; + +use super::{HyperServer, HyperServerOptions}; + +/// Creates a new HyperServer instance with the provided handler and options +/// The handler must implement ServerHandler. +/// +/// # Arguments +/// * `server_details` - Initialization result from the MCP schema +/// * `handler` - Implementation of the ServerHandlerCore trait +/// * `server_options` - Configuration options for the HyperServer +/// +/// # Returns +/// * `HyperServer` - A configured HyperServer instance ready to start +pub fn create_server( + server_details: InitializeResult, + handler: impl ServerHandler, + server_options: HyperServerOptions, +) -> HyperServer { + HyperServer::new( + server_details, + Arc::new(ServerRuntimeInternalHandler::new(Box::new(handler))), + server_options, + ) +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs new file mode 100644 index 0000000..d0663f3 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs @@ -0,0 +1,26 @@ +use super::{HyperServer, HyperServerOptions}; +use crate::mcp_server::{server_runtime_core::RuntimeCoreInternalHandler, ServerHandlerCore}; +use rust_mcp_schema::InitializeResult; +use std::sync::Arc; + +/// Creates a new HyperServer instance with the provided handler and options +/// The handler must implement ServerHandlerCore. +/// +/// # Arguments +/// * `server_details` - Initialization result from the MCP schema +/// * `handler` - Implementation of the ServerHandlerCore trait +/// * `server_options` - Configuration options for the HyperServer +/// +/// # Returns +/// * `HyperServer` - A configured HyperServer instance ready to start +pub fn create_server( + server_details: InitializeResult, + handler: impl ServerHandlerCore, + server_options: HyperServerOptions, +) -> HyperServer { + HyperServer::new( + server_details, + Arc::new(RuntimeCoreInternalHandler::new(Box::new(handler))), + server_options, + ) +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes.rs new file mode 100644 index 0000000..7146880 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes.rs @@ -0,0 +1,29 @@ +pub mod fallback_routes; +pub mod messages_routes; +pub mod sse_routes; + +use super::{app_state::AppState, HyperServerOptions}; +use axum::Router; +use std::sync::Arc; + +/// Constructs the Axum router with all application routes +/// +/// Combines routes for Server-Sent Events, message handling, and fallback routes, +/// attaching the shared application state to the router. +/// +/// # Arguments +/// * `state` - Shared application state wrapped in an Arc +/// * `server_options` - Reference to the HyperServer configuration options +/// +/// # Returns +/// * `Router` - An Axum router configured with all application routes and state +pub fn app_routes(state: Arc, server_options: &HyperServerOptions) -> Router { + Router::new() + .merge(sse_routes::routes( + state.clone(), + server_options.sse_endpoint(), + )) + .merge(messages_routes::routes(state.clone())) + .with_state(state) + .merge(fallback_routes::routes()) +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/routes/fallback_routes.rs b/crates/rust-mcp-sdk/src/hyper_servers/routes/fallback_routes.rs new file mode 100644 index 0000000..d6ae240 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/fallback_routes.rs @@ -0,0 +1,15 @@ +use axum::{ + http::{StatusCode, Uri}, + Router, +}; + +pub fn routes() -> Router { + Router::new().fallback(not_found) +} + +pub async fn not_found(uri: Uri) -> (StatusCode, String) { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Server Error!\r\n uri: {}", uri), + ) +} 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 new file mode 100644 index 0000000..10e2eb9 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/messages_routes.rs @@ -0,0 +1,50 @@ +use crate::hyper_servers::{ + app_state::AppState, + error::{TransportServerError, TransportServerResult}, +}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, + routing::post, + Router, +}; +use std::{collections::HashMap, sync::Arc}; +use tokio::io::AsyncWriteExt; + +const SSE_MESSAGES_PATH: &str = "/messages"; + +pub fn routes(_state: Arc) -> Router> { + Router::new().route(SSE_MESSAGES_PATH, post(handle_messages)) +} + +pub async fn handle_messages( + State(state): State>, + Query(params): Query>, + message: String, +) -> TransportServerResult { + let session_id = params + .get("sessionId") + .ok_or(TransportServerError::SessionIdMissing)?; + + let transmit = + state + .session_store + .get(session_id) + .await + .ok_or(TransportServerError::SessionIdInvalid( + session_id.to_string(), + ))?; + let mut transmit = transmit.lock().await; + + transmit + .write_all(format!("{message}\n").as_bytes()) + .await + .map_err(|err| TransportServerError::StreamIoError(err.to_string()))?; + + transmit + .flush() + .await + .map_err(|err| TransportServerError::StreamIoError(err.to_string()))?; + + Ok(axum::http::StatusCode::OK) +} 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 new file mode 100644 index 0000000..2efe3be --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/routes/sse_routes.rs @@ -0,0 +1,167 @@ +use crate::{ + error::McpSdkError, + hyper_servers::{app_state::AppState, error::TransportServerResult}, + mcp_server::{server_runtime, ServerRuntime}, + mcp_traits::mcp_handler::McpServerHandler, + McpServer, +}; +use axum::{ + extract::State, + response::{ + sse::{Event, KeepAlive}, + IntoResponse, Sse, + }, + routing::get, + Router, +}; +use futures::stream::{self}; +use rust_mcp_transport::{error::TransportError, SseTransport}; +use std::{convert::Infallible, sync::Arc, time::Duration}; +use tokio::{ + io::{duplex, AsyncBufReadExt, BufReader}, + time::{self, Interval}, +}; +use tokio_stream::StreamExt; + +const SSE_MESSAGES_PATH: &str = "/messages"; +const CLIENT_PING_TIMEOUT: Duration = Duration::from_secs(2); + +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(session_id: &str) -> Result { + Ok(Event::default() + .event("endpoint") + .data(format!("{SSE_MESSAGES_PATH}?sessionId={session_id}"))) +} + +/// Configures the SSE routes for the application +/// +/// Sets up the Axum router with a single GET route for the specified SSE endpoint. +/// +/// # Arguments +/// * `_state` - Shared application state (not used directly in routing) +/// * `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)) +} + +/// Handles Server-Sent Events (SSE) connections +/// +/// Establishes an SSE connection, sets up a server instance, and streams messages +/// to the client. Manages session creation, periodic pings, and server lifecycle. +/// +/// # Arguments +/// * `State(state)` - Extracted application state +/// +/// # Returns +/// * `TransportServerResult` - The SSE response stream or an error +pub async fn handle_sse( + State(state): State>, +) -> 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); + + // generate a session id, and keep it in the server state + let session_id = state.id_generator.generate(); + state + .session_store + .set(session_id.to_owned(), read_tx) + .await; + + // create a transport for sending/receiving messages + let transport = + SseTransport::new(read_rx, write_tx, Arc::clone(&state.transport_options)).unwrap(); + let d: Arc = state.handler.clone(); + // create a new server instance with unique session_id and + let server: Arc = Arc::new(server_runtime::create_server_instance( + Arc::clone(&state.server_details), + transport, + d, + session_id.to_owned(), + )); + + // Ping the server periodically to check if the SSE client is still connected + let server_ping = Arc::clone(&server); + tokio::spawn(async move { + let mut interval: Interval = time::interval(state.ping_interval); + loop { + interval.tick().await; // Wait for the next tick (10 seconds) + if !server_ping.is_initialized() { + continue; + } + match server_ping.ping(Some(CLIENT_PING_TIMEOUT)).await { + Ok(_) => {} + Err(McpSdkError::TransportError(TransportError::StdioError(error))) => { + if error.kind() == std::io::ErrorKind::BrokenPipe { + if let Some(session_id) = server_ping.session_id().await { + tracing::info!("Stopping {} server task ...", session_id); + state.session_store.delete(&session_id).await; + break; + } + } + } + _ => {} + } + } + }); + + tracing::info!( + "A new client joined : {}", + server.session_id().await.unwrap_or_default().to_owned() + ); + + // Start the server + tokio::spawn(async move { + match server.start().await { + Ok(_) => tracing::info!( + "server {} exited gracefully.", + server.session_id().await.unwrap_or_default().to_owned() + ), + Err(err) => tracing::info!( + "server {} exited with error : {}", + server.session_id().await.unwrap_or_default().to_owned(), + err + ), + } + }); + + // Initial SSE message to inform the client about the server's endpoint + let initial_event = stream::once(async move { initial_event(&session_id) }); + + // Construct SSE stream for sending MCP messages to the server + 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) +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/server.rs b/crates/rust-mcp-sdk/src/hyper_servers/server.rs new file mode 100644 index 0000000..f0770e1 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/server.rs @@ -0,0 +1,312 @@ +use crate::mcp_traits::mcp_handler::McpServerHandler; +#[cfg(feature = "ssl")] +use axum_server::tls_rustls::RustlsConfig; +use std::{ + net::{SocketAddr, ToSocketAddrs}, + path::Path, + sync::Arc, + time::Duration, +}; + +use super::{ + app_state::AppState, + error::{TransportServerError, TransportServerResult}, + routes::app_routes, + InMemorySessionStore, UuidGenerator, +}; +use axum::Router; +use rust_mcp_schema::InitializeResult; +use rust_mcp_transport::TransportOptions; + +// Default client ping interval (12 seconds) +const DEFAULT_CLIENT_PING_INTERVAL: Duration = Duration::from_secs(12); + +// Default Server-Sent Events (SSE) endpoint path +const DEFAULT_SSE_ENDPOINT: &str = "/sse"; + +/// Configuration struct for the Hyper server +/// Used to configure the HyperServer instance. +pub struct HyperServerOptions { + /// Hostname or IP address the server will bind to (default: "localhost") + pub host: String, + /// Hostname or IP address the server will bind to (default: "localhost") + pub port: u16, + /// Optional custom path for the Server-Sent Events (SSE) endpoint. + /// If `None`, the default path `/sse` will be used. + pub custom_sse_endpoint: Option, + /// Interval between automatic ping messages sent to clients to detect disconnects + pub ping_interval: Duration, + /// Enables SSL/TLS if set to `true` + pub enable_ssl: bool, + /// Path to the SSL/TLS certificate file (e.g., "cert.pem"). + /// Required if `enable_ssl` is `true`. + pub ssl_cert_path: Option, + /// Path to the SSL/TLS private key file (e.g., "key.pem"). + /// Required if `enable_ssl` is `true`. + pub ssl_key_path: Option, + /// Shared transport configuration used by the server + pub transport_options: Arc, +} + +impl HyperServerOptions { + /// Validates the server configuration options + /// + /// Ensures that SSL-related paths are provided and valid when SSL is enabled. + /// + /// # Returns + /// * `TransportServerResult<()>` - Ok if validation passes, Err with TransportServerError if invalid + pub fn validate(&self) -> TransportServerResult<()> { + if self.enable_ssl { + if self.ssl_cert_path.is_none() || self.ssl_key_path.is_none() { + return Err(TransportServerError::InvalidServerOptions( + "Both 'ssl_cert_path' and 'ssl_key_path' must be provided when SSL is enabled." + .into(), + )); + } + + if !Path::new(self.ssl_cert_path.as_deref().unwrap_or("")).is_file() { + return Err(TransportServerError::InvalidServerOptions( + "'ssl_cert_path' does not point to a valid or existing file.".into(), + )); + } + + if !Path::new(self.ssl_key_path.as_deref().unwrap_or("")).is_file() { + return Err(TransportServerError::InvalidServerOptions( + "'ssl_key_path' does not point to a valid or existing file.".into(), + )); + } + } + + Ok(()) + } + + /// Resolves the server address from host and port + /// + /// Validates the configuration and converts the host/port into a SocketAddr. + /// Handles scheme prefixes (http:// or https://) and logs warnings for mismatches. + /// + /// # Returns + /// * `TransportServerResult` - The resolved server address or an error + async fn resolve_server_address(&self) -> TransportServerResult { + self.validate()?; + + let mut host = self.host.to_string(); + if let Some(stripped) = self.host.strip_prefix("http://") { + if self.enable_ssl { + tracing::warn!("Warning: Ignoring http:// scheme for SSL; using hostname only"); + } + host = stripped.to_string(); + } else if let Some(stripped) = host.strip_prefix("https://") { + host = stripped.to_string(); + } + + let addr = { + let mut iter = (host, self.port) + .to_socket_addrs() + .map_err(|err| TransportServerError::ServerStartError(err.to_string()))?; + match iter.next() { + Some(addr) => addr, + None => format!("{}:{}", self.host, self.port).parse().map_err( + |err: std::net::AddrParseError| { + TransportServerError::ServerStartError(err.to_string()) + }, + )?, + } + }; + Ok(addr) + } + + pub fn sse_endpoint(&self) -> &str { + self.custom_sse_endpoint + .as_deref() + .unwrap_or(DEFAULT_SSE_ENDPOINT) + } +} + +/// Default implementation for HyperServerOptions +/// +/// Provides default values for the server configuration, including localhost address, +/// port 8080, default SSE endpoint, and 12-second ping interval. +impl Default for HyperServerOptions { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 8080, + custom_sse_endpoint: None, + ping_interval: DEFAULT_CLIENT_PING_INTERVAL, + transport_options: Default::default(), + enable_ssl: false, + ssl_cert_path: None, + ssl_key_path: None, + } + } +} + +/// Hyper server struct for managing the Axum-based web server +pub struct HyperServer { + app: Router, + state: Arc, + options: HyperServerOptions, +} + +impl HyperServer { + /// Creates a new HyperServer instance + /// + /// Initializes the server with the provided server details, handler, and options. + /// + /// # Arguments + /// * `server_details` - Initialization result from the MCP schema + /// * `handler` - Shared MCP server handler with static lifetime + /// * `server_options` - Server configuration options + /// + /// # Returns + /// * `Self` - A new HyperServer instance + pub(crate) fn new( + server_details: InitializeResult, + handler: Arc, + server_options: HyperServerOptions, + ) -> Self { + let state: Arc = Arc::new(AppState { + session_store: Arc::new(InMemorySessionStore::new()), + id_generator: Arc::new(UuidGenerator {}), + server_details: Arc::new(server_details), + handler, + ping_interval: server_options.ping_interval, + transport_options: Arc::clone(&server_options.transport_options), + }); + let app = app_routes(Arc::clone(&state), &server_options); + Self { + app, + state, + options: server_options, + } + } + + /// Returns a shared reference to the application state + /// + /// # Returns + /// * `Arc` - Shared application state + pub fn state(&self) -> Arc { + Arc::clone(&self.state) + } + + /// Adds a new route to the server + /// + /// # Arguments + /// * `path` - The route path (static string) + /// * `route` - The Axum MethodRouter for handling the route + /// + /// # Returns + /// * `Self` - The modified HyperServer instance + pub fn with_route(mut self, path: &'static str, route: axum::routing::MethodRouter) -> Self { + self.app = self.app.route(path, route); + self + } + + /// Generates server information string + /// + /// Constructs a string describing the server type, protocol, address, and SSE endpoint. + /// + /// # Arguments + /// * `addr` - Optional SocketAddr; if None, resolves from options + /// + /// # Returns + /// * `TransportServerResult` - The server information string or an error + pub async fn server_info(&self, addr: Option) -> TransportServerResult { + let addr = addr.unwrap_or(self.options.resolve_server_address().await?); + let server_type = if self.options.enable_ssl { + "SSL server" + } else { + "Server" + }; + let protocol = if self.options.enable_ssl { + "https" + } else { + "http" + }; + + let server_url = format!( + "{} is available at {}://{}{}", + server_type, + protocol, + addr, + self.options.sse_endpoint() + ); + + Ok(server_url) + } + + // pub fn with_layer(mut self, layer: L) -> Self + // where + // // L: Layer + Clone + Send + Sync + 'static, + // L::Service: Send + Sync + 'static, + // { + // self.router = self.router.layer(layer); + // self + // } + + /// Starts the server with SSL support (available when "ssl" feature is enabled) + /// + /// # Arguments + /// * `addr` - The server address to bind to + /// + /// # Returns + /// * `TransportServerResult<()>` - Ok if the server starts successfully, Err otherwise + #[cfg(feature = "ssl")] + async fn start_ssl(self, addr: SocketAddr) -> TransportServerResult<()> { + let config = RustlsConfig::from_pem_file( + self.options.ssl_cert_path.as_deref().unwrap_or_default(), + self.options.ssl_key_path.as_deref().unwrap_or_default(), + ) + .await + .map_err(|err| TransportServerError::SslCertError(err.to_string()))?; + + tracing::info!("{}", self.server_info(Some(addr)).await?); + + axum_server::bind_rustls(addr, config) + .serve(self.app.into_make_service()) + .await + .map_err(|err| TransportServerError::ServerStartError(err.to_string())) + } + + /// Starts the server without SSL + /// + /// # Arguments + /// * `addr` - The server address to bind to + /// + /// # Returns + /// * `TransportServerResult<()>` - Ok if the server starts successfully, Err otherwise + async fn start_http(self, addr: SocketAddr) -> TransportServerResult<()> { + tracing::info!("{}", self.server_info(Some(addr)).await?); + + axum_server::bind(addr) + .serve(self.app.into_make_service()) + .await + .map_err(|err| TransportServerError::ServerStartError(err.to_string())) + } + + /// Starts the server, choosing SSL or HTTP based on configuration + /// + /// Resolves the server address and starts the server in either SSL or HTTP mode. + /// Panics if SSL is requested but the "ssl" feature is not enabled. + /// + /// # Returns + /// * `TransportServerResult<()>` - Ok if the server starts successfully, Err otherwise + pub async fn start(self) -> TransportServerResult<()> { + let addr = self.options.resolve_server_address().await?; + + #[cfg(feature = "ssl")] + if self.options.enable_ssl { + self.start_ssl(addr).await + } else { + self.start_http(addr).await + } + + #[cfg(not(feature = "ssl"))] + if self.options.enable_ssl { + panic!("SSL requested but the 'ssl' feature is not enabled"); + } else { + self.start_http(addr).await + } + } +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/session_store.rs b/crates/rust-mcp-sdk/src/hyper_servers/session_store.rs new file mode 100644 index 0000000..da25000 --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/session_store.rs @@ -0,0 +1,65 @@ +mod in_memory; +use std::sync::Arc; + +use async_trait::async_trait; +pub use in_memory::*; +use tokio::{io::DuplexStream, sync::Mutex}; +use uuid::Uuid; + +// Type alias for the server-side duplex stream used in sessions +pub type TxServer = DuplexStream; + +// Type alias for session identifier, represented as a String +pub type SessionId = String; + +/// Trait defining the interface for session storage operations +/// +/// This trait provides asynchronous methods for managing session data, +/// Implementors must be Send and Sync to support concurrent access. +#[async_trait] +pub trait SessionStore: Send + Sync { + /// Retrieves a session by its identifier + /// + /// # Arguments + /// * `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>>; + /// 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); + /// 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); +} + +/// Trait for generating session identifiers +/// +/// Implementors must be Send and Sync to support concurrent access. +pub trait IdGenerator: Send + Sync { + fn generate(&self) -> SessionId; +} + +/// Struct implementing the IdGenerator trait using UUID v4 +/// +/// This is a simple wrapper around the uuid crate's Uuid::new_v4 function +/// to generate unique session identifiers. +pub struct UuidGenerator {} + +impl IdGenerator for UuidGenerator { + /// Generates a new UUID v4-based session identifier + /// + /// # Returns + /// * `SessionId` - A new UUID-based session identifier as a String + fn generate(&self) -> SessionId { + Uuid::new_v4().to_string() + } +} diff --git a/crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs b/crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs new file mode 100644 index 0000000..7c5755d --- /dev/null +++ b/crates/rust-mcp-sdk/src/hyper_servers/session_store/in_memory.rs @@ -0,0 +1,57 @@ +use super::SessionId; +use super::{SessionStore, TxServer}; +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>>>>, +} + +impl InMemorySessionStore { + /// Creates a new in-memory session store + /// + /// Initializes an empty HashMap wrapped in a read-write lock for thread-safe access. + /// + /// # Returns + /// * `Self` - A new InMemorySessionStore instance + pub fn new() -> Self { + Self { + store: Arc::new(RwLock::new(HashMap::new())), + } + } +} + +/// 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>> { + let store = self.store.read().await; + store.get(key).cloned() + } + + async fn set(&self, key: SessionId, value: TxServer) { + let mut store = self.store.write().await; + store.insert(key, Arc::new(Mutex::new(value))); + } + + async fn delete(&self, key: &SessionId) { + let mut store = self.store.write().await; + store.remove(key); + } + + async fn clear(&self) { + let mut store = self.store.write().await; + store.clear(); + } +} diff --git a/crates/rust-mcp-sdk/src/lib.rs b/crates/rust-mcp-sdk/src/lib.rs index ab7965c..206b92c 100644 --- a/crates/rust-mcp-sdk/src/lib.rs +++ b/crates/rust-mcp-sdk/src/lib.rs @@ -1,4 +1,6 @@ pub mod error; +#[cfg(feature = "hyper-server")] +mod hyper_servers; mod mcp_handlers; mod mcp_macros; mod mcp_runtimes; @@ -17,8 +19,8 @@ pub mod mcp_client { //! it works with `mcp_server_handler` trait //! that offers default implementation for common messages like handling initialization or //! responding to ping requests, so you only need to override and customize the handler - //! functions relevant to your specific needs. - //! + //! functions relevant to your specific needs. + //! //! Refer to [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) for an example. //! //! @@ -48,8 +50,8 @@ pub mod mcp_server { //! it works with `mcp_server_handler` trait //! that offers default implementation for common messages like handling initialization or //! responding to ping requests, so you only need to override and customize the handler - //! functions relevant to your specific needs. - //! + //! functions relevant to your specific needs. + //! //! Refer to [examples/hello-world-mcp-server](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server) for an example. //! //! @@ -66,6 +68,13 @@ pub mod mcp_server { pub use super::mcp_runtimes::server_runtime::mcp_server_runtime as server_runtime; pub use super::mcp_runtimes::server_runtime::mcp_server_runtime_core as server_runtime_core; pub use super::mcp_runtimes::server_runtime::ServerRuntime; + + #[cfg(feature = "hyper-server")] + pub use super::hyper_servers::hyper_server; + #[cfg(feature = "hyper-server")] + pub use super::hyper_servers::hyper_server_core; + #[cfg(feature = "hyper-server")] + pub use super::hyper_servers::*; } #[cfg(feature = "client")] diff --git a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime.rs b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime.rs index b6828a6..d325305 100644 --- a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime.rs +++ b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime.rs @@ -12,6 +12,8 @@ use std::sync::{Arc, RwLock}; use tokio::io::AsyncWriteExt; use crate::error::SdkResult; +#[cfg(feature = "hyper-server")] +use crate::hyper_servers::SessionId; use crate::mcp_traits::mcp_handler::McpServerHandler; use crate::mcp_traits::mcp_server::McpServer; @@ -20,14 +22,16 @@ pub struct ServerRuntime { // The transport interface for handling messages between client and server transport: Box>, // The handler for processing MCP messages - handler: Box, + handler: Arc, // Information about the server - server_details: InitializeResult, + server_details: Arc, // Details about the connected client client_details: Arc>>, message_sender: tokio::sync::RwLock>>, error_stream: tokio::sync::RwLock>>>, + #[cfg(feature = "hyper-server")] + session_id: Option, } #[async_trait] @@ -143,6 +147,11 @@ impl ServerRuntime { *lock = Some(sender); } + #[cfg(feature = "hyper-server")] + pub(crate) async fn session_id(&self) -> Option { + self.session_id.to_owned() + } + pub(crate) async fn set_error_stream( &self, error_stream: Pin>, @@ -151,18 +160,38 @@ impl ServerRuntime { *lock = Some(error_stream); } + #[cfg(feature = "hyper-server")] + pub(crate) fn new_instance( + server_details: Arc, + transport: impl Transport, + handler: Arc, + session_id: SessionId, + ) -> Self { + Self { + server_details, + client_details: Arc::new(RwLock::new(None)), + transport: Box::new(transport), + handler, + message_sender: tokio::sync::RwLock::new(None), + error_stream: tokio::sync::RwLock::new(None), + session_id: Some(session_id), + } + } + pub(crate) fn new( server_details: InitializeResult, transport: impl Transport, - handler: Box, + handler: Arc, ) -> Self { Self { - server_details, + server_details: Arc::new(server_details), client_details: Arc::new(RwLock::new(None)), transport: Box::new(transport), handler, message_sender: tokio::sync::RwLock::new(None), error_stream: tokio::sync::RwLock::new(None), + #[cfg(feature = "hyper-server")] + session_id: None, } } } diff --git a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs index fde0bd4..dd9e98f 100644 --- a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs +++ b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use async_trait::async_trait; use rust_mcp_schema::{ schema_utils::{ @@ -8,14 +10,16 @@ use rust_mcp_schema::{ }; use rust_mcp_transport::Transport; +use super::ServerRuntime; +#[cfg(feature = "hyper-server")] +use crate::hyper_servers::SessionId; + use crate::{ error::SdkResult, mcp_handlers::mcp_server_handler::ServerHandler, mcp_traits::{mcp_handler::McpServerHandler, mcp_server::McpServer}, }; -use super::ServerRuntime; - /// Creates a new MCP server runtime with the specified configuration. /// /// This function initializes a server for (MCP) by accepting server details, transport , @@ -42,11 +46,21 @@ pub fn create_server( ServerRuntime::new( server_details, transport, - Box::new(ServerRuntimeInternalHandler::new(Box::new(handler))), + Arc::new(ServerRuntimeInternalHandler::new(Box::new(handler))), ) } -struct ServerRuntimeInternalHandler { +#[cfg(feature = "hyper-server")] +pub(crate) fn create_server_instance( + server_details: Arc, + transport: impl Transport, + handler: Arc, + session_id: SessionId, +) -> ServerRuntime { + ServerRuntime::new_instance(server_details, transport, handler, session_id) +} + +pub(crate) struct ServerRuntimeInternalHandler { handler: H, } impl ServerRuntimeInternalHandler> { diff --git a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs index baec424..a61d0c5 100644 --- a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs +++ b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use async_trait::async_trait; use rust_mcp_schema::schema_utils::{ self, ClientMessage, MessageFromServer, NotificationFromClient, RequestFromClient, @@ -39,11 +41,11 @@ pub fn create_server( ServerRuntime::new( server_details, transport, - Box::new(RuntimeCoreInternalHandler::new(Box::new(handler))), + Arc::new(RuntimeCoreInternalHandler::new(Box::new(handler))), ) } -struct RuntimeCoreInternalHandler { +pub(crate) struct RuntimeCoreInternalHandler { handler: H, } diff --git a/crates/rust-mcp-sdk/tests/test_client_runtime.rs b/crates/rust-mcp-sdk/tests/test_client_runtime.rs index dfe40bf..c7804e5 100644 --- a/crates/rust-mcp-sdk/tests/test_client_runtime.rs +++ b/crates/rust-mcp-sdk/tests/test_client_runtime.rs @@ -25,8 +25,8 @@ async fn tets_client_launch_npx_server() { let server_capabilities = client.server_capabilities().unwrap(); let server_info = client.server_info().unwrap(); - assert!(server_info.server_info.name.len() > 0); - assert!(server_info.server_info.version.len() > 0); + assert!(!server_info.server_info.name.is_empty()); + assert!(!server_info.server_info.version.is_empty()); assert!(server_capabilities.tools.is_some()); } @@ -50,7 +50,7 @@ async fn tets_client_launch_uvx_server() { let server_capabilities = client.server_capabilities().unwrap(); let server_info = client.server_info().unwrap(); - assert!(server_info.server_info.name.len() > 0); - assert!(server_info.server_info.version.len() > 0); + assert!(!server_info.server_info.name.is_empty()); + assert!(!server_info.server_info.version.is_empty()); assert!(server_capabilities.tools.is_some()); } diff --git a/crates/rust-mcp-transport/Cargo.toml b/crates/rust-mcp-transport/Cargo.toml index a7e658c..9ac1818 100644 --- a/crates/rust-mcp-transport/Cargo.toml +++ b/crates/rust-mcp-transport/Cargo.toml @@ -19,6 +19,25 @@ futures = { workspace = true } thiserror = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } +axum = { workspace = true } +uuid = { workspace = true, features = ["v4"] } +tokio-stream = { workspace = true } +reqwest = { workspace = true, features = ["stream"] } +bytes = { workspace = true } +tracing = { workspace = true } +[dev-dependencies] +wiremock = "0.5" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +futures = { workspace = true } [lints] workspace = true + + +# ### FEATURES ################################################################# +# [features] + +# default = ["stdio", "sse"] # Default features + +# stdio = [] +# sse = [] diff --git a/crates/rust-mcp-transport/src/Cargo.toml b/crates/rust-mcp-transport/src/Cargo.toml new file mode 100644 index 0000000..9b7096c --- /dev/null +++ b/crates/rust-mcp-transport/src/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "simple-mcp-client" +version = "0.1.9" +edition = "2021" +publish = false +license = "MIT" + + +[dependencies] +rust-mcp-sdk = { workspace = true, default-features = false, features = [ + "client", + "macros", +] } +rust-mcp-schema = { workspace = true } + +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +colored = "3.0.0" + +[lints] +workspace = true diff --git a/crates/rust-mcp-transport/src/client_sse.rs b/crates/rust-mcp-transport/src/client_sse.rs new file mode 100644 index 0000000..a1a7e3e --- /dev/null +++ b/crates/rust-mcp-transport/src/client_sse.rs @@ -0,0 +1,353 @@ +use crate::error::{TransportError, TransportResult}; +use crate::mcp_stream::MCPStream; +use crate::message_dispatcher::MessageDispatcher; +use crate::transport::Transport; +use crate::utils::{ + extract_origin, http_post, CancellationTokenSource, ReadableChannel, SseStream, WritableChannel, +}; +use crate::{IoStream, McpDispatch, TransportOptions}; +use async_trait::async_trait; +use bytes::Bytes; +use futures::Stream; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::Client; +use rust_mcp_schema::schema_utils::{McpMessage, RpcMessage}; +use rust_mcp_schema::RequestId; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::io::{BufReader, BufWriter}; +use tokio::sync::{mpsc, oneshot, Mutex}; + +const DEFAULT_CHANNEL_CAPACITY: usize = 64; +const DEFAULT_MAX_RETRY: usize = 5; +const DEFAULT_RETRY_TIME_SECONDS: u64 = 3; +const SHUTDOWN_TIMEOUT_SECONDS: u64 = 5; + +/// Configuration options for the Client SSE Transport +/// +/// Defines settings for request timeouts, retry behavior, and custom HTTP headers. +pub struct ClientSseTransportOptions { + pub request_timeout: Duration, + pub retry_delay: Option, + pub max_retries: Option, + pub custom_headers: Option>, +} + +/// Provides default values for ClientSseTransportOptions +impl Default for ClientSseTransportOptions { + fn default() -> Self { + Self { + request_timeout: TransportOptions::default().timeout, + retry_delay: None, + max_retries: None, + custom_headers: None, + } + } +} + +/// Client-side Server-Sent Events (SSE) transport implementation +/// +/// Manages SSE connections, HTTP POST requests, and message streaming for client-server communication. +pub struct ClientSseTransport { + /// Optional cancellation token source for shutting down the transport + shutdown_source: tokio::sync::RwLock>, + /// Flag indicating if the transport is shut down + is_shut_down: Mutex, + /// Timeout duration for MCP messages + request_timeout: Duration, + /// HTTP client for making requests + client: Client, + /// URL for the SSE endpoint + sse_url: String, + /// Base URL extracted from the server URL + base_url: String, + /// Delay between retry attempts + retry_delay: Duration, + /// Maximum number of retry attempts + max_retries: usize, + /// Optional custom HTTP headers + custom_headers: Option, + sse_task: tokio::sync::RwLock>>, + post_task: tokio::sync::RwLock>>, +} + +impl ClientSseTransport { + /// Creates a new ClientSseTransport instance + /// + /// Initializes the transport with the provided server URL and options. + /// + /// # Arguments + /// * `server_url` - The URL of the SSE server + /// * `options` - Configuration options for the transport + /// + /// # Returns + /// * `TransportResult` - The initialized transport or an error + pub fn new(server_url: &str, options: ClientSseTransportOptions) -> TransportResult { + let client = Client::new(); + + //TODO: error handling + let base_url = extract_origin(server_url).unwrap(); + + let headers = match &options.custom_headers { + Some(h) => Some(Self::validate_headers(h)?), + None => None, + }; + + Ok(Self { + client, + base_url, + sse_url: server_url.to_string(), + max_retries: options.max_retries.unwrap_or(DEFAULT_MAX_RETRY), + retry_delay: options + .retry_delay + .unwrap_or(Duration::from_secs(DEFAULT_RETRY_TIME_SECONDS)), + shutdown_source: tokio::sync::RwLock::new(None), + is_shut_down: Mutex::new(false), + request_timeout: options.request_timeout, + custom_headers: headers, + sse_task: tokio::sync::RwLock::new(None), + post_task: tokio::sync::RwLock::new(None), + }) + } + + /// Validates and converts a HashMap of headers into a HeaderMap + /// + /// # Arguments + /// * `headers` - The HashMap of header names and values + /// + /// # Returns + /// * `TransportResult` - The validated HeaderMap or an error + fn validate_headers(headers: &HashMap) -> TransportResult { + let mut header_map = HeaderMap::new(); + + for (key, value) in headers { + let header_name = key.parse::().map_err(|e| { + TransportError::InvalidOptions(format!("Invalid header name: {}", e)) + })?; + let header_value = HeaderValue::from_str(value).map_err(|e| { + TransportError::InvalidOptions(format!("Invalid header value: {}", e)) + })?; + header_map.insert(header_name, header_value); + } + + Ok(header_map) + } + + /// Validates the message endpoint URL + /// + /// Ensures the endpoint is either relative to the base URL or matches the base URL's origin. + /// + /// # Arguments + /// * `endpoint` - The endpoint URL to validate + /// + /// # Returns + /// * `TransportResult` - The validated endpoint URL or an error + pub fn validate_message_endpoint(&self, endpoint: String) -> TransportResult { + if endpoint.starts_with("/") { + return Ok(format!("{}{}", self.base_url, endpoint)); + } + if let Some(endpoint_origin) = extract_origin(&endpoint) { + if endpoint_origin.cmp(&self.base_url) != Ordering::Equal { + return Err(TransportError::InvalidOptions(format!( + "Endpoint origin does not match connection origin. expected: {} , received: {}", + self.base_url, endpoint_origin + ))); + } + return Ok(endpoint); + } + Ok(endpoint) + } +} + +#[async_trait] +impl Transport for ClientSseTransport +where + R: RpcMessage + Clone + Send + Sync + serde::de::DeserializeOwned + 'static, + S: McpMessage + Clone + Send + Sync + serde::Serialize + 'static, +{ + /// Starts the transport, initializing SSE and POST tasks + /// + /// Sets up the SSE stream, POST request handler, and message streams for communication. + /// + /// # Returns + /// * `TransportResult<(Pin + Send>>, MessageDispatcher, IoStream)>` + /// - The message stream, dispatcher, and error stream + async fn start( + &self, + ) -> TransportResult<( + Pin + Send>>, + MessageDispatcher, + IoStream, + )> + where + MessageDispatcher: McpDispatch, + { + // Create CancellationTokenSource and token + let (cancellation_source, cancellation_token) = CancellationTokenSource::new(); + let mut lock = self.shutdown_source.write().await; + *lock = Some(cancellation_source); + + let pending_requests: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); + + let (write_tx, mut write_rx) = mpsc::channel::(DEFAULT_CHANNEL_CAPACITY); + let (read_tx, read_rx) = mpsc::channel::(DEFAULT_CHANNEL_CAPACITY); + + // Create oneshot channel for signaling SSE endpoint event message + let (endpoint_event_tx, endpoint_event_rx) = oneshot::channel::>(); + let endpoint_event_tx = Some(endpoint_event_tx); + + let sse_client = self.client.clone(); + let sse_url = self.sse_url.clone(); + + let max_retries = self.max_retries; + let retry_delay = self.retry_delay; + + let read_stream = SseStream { + sse_client, + sse_url, + max_retries, + retry_delay, + read_tx, + }; + + // Spawn task to handle SSE stream with reconnection + let cancellation_token_sse = cancellation_token.clone(); + let sse_task_handle = tokio::spawn(async move { + read_stream + .run(endpoint_event_tx, cancellation_token_sse) + .await; + }); + let mut sse_task_lock = self.sse_task.write().await; + *sse_task_lock = Some(sse_task_handle); + + // Await the first SSE message, expected to receive messages endpoint from he server + let err = || { + std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to receive 'messages' endpoint from the server.", + ) + }; + let post_url = endpoint_event_rx + .await + .map_err(|_| err())? + .ok_or_else(err)?; + + let post_url = self.validate_message_endpoint(post_url)?; + + let client_clone = self.client.clone(); + + let custom_headers = self.custom_headers.clone(); + + let cancellation_token_post = cancellation_token.clone(); + // Spawn task to handle POST requests from writable stream + let post_task_handle = tokio::spawn(async move { + loop { + tokio::select! { + + _ = cancellation_token_post.cancelled() => + { + break; + }, + + data = write_rx.recv() => { + match data{ + Some(data) => { + // trim the trailing \n before making a request + let body = String::from_utf8_lossy(&data).trim().to_string(); + if let Err(e) = http_post(&client_clone, &post_url, body, &custom_headers).await { + eprintln!("Failed to POST message: {:?}", e); + } + }, + None => break, // Exit if channel is closed + } + } + } + } + }); + let mut post_task_lock = self.post_task.write().await; + *post_task_lock = Some(post_task_handle); + + // Create writable stream + let writable: Mutex>> = + Mutex::new(Box::pin(BufWriter::new(WritableChannel { write_tx }))); + + // Create readable stream + let readable: Pin> = + Box::pin(BufReader::new(ReadableChannel { + read_rx, + buffer: Bytes::new(), + })); + + let (stream, sender, error_stream) = MCPStream::create( + readable, + writable, + IoStream::Writable(Box::pin(tokio::io::stderr())), + pending_requests, + self.request_timeout, + cancellation_token, + ); + + Ok((stream, sender, error_stream)) + } + + /// Checks if the transport has been shut down + /// + /// # Returns + /// * `bool` - True if the transport is shut down, false otherwise + async fn is_shut_down(&self) -> bool { + let result = self.is_shut_down.lock().await; + *result + } + + // Shuts down the transport, terminating any subprocess and signaling closure. + /// + /// Sends a shutdown signal via the watch channel and kills the subprocess if present. + /// + /// # Returns + /// A `TransportResult` indicating success or failure. + /// + /// # Errors + /// Returns a `TransportError` if the shutdown signal fails or the process cannot be killed. + async fn shut_down(&self) -> TransportResult<()> { + // Trigger cancellation + let mut cancellation_lock = self.shutdown_source.write().await; + if let Some(source) = cancellation_lock.as_ref() { + source.cancel()?; + } + *cancellation_lock = None; // Clear cancellation_source + + // Mark as shut down + let mut is_shut_down_lock = self.is_shut_down.lock().await; + *is_shut_down_lock = true; + + // Get task handles + let sse_task = self.sse_task.write().await.take(); + let post_task = self.post_task.write().await.take(); + + // Wait for tasks to complete with a timeout + let timeout = Duration::from_secs(SHUTDOWN_TIMEOUT_SECONDS); + let shutdown_future = async { + if let Some(post_handle) = post_task { + let _ = post_handle.await; + } + if let Some(sse_handle) = sse_task { + let _ = sse_handle.await; + } + Ok::<(), TransportError>(()) + }; + + tokio::select! { + result = shutdown_future => { + result // result of task completion + } + _ = tokio::time::sleep(timeout) => { + tracing::warn!("Shutdown timed out after {:?}", timeout); + Err(TransportError::ShutdownTimeout) + } + } + } +} diff --git a/crates/rust-mcp-transport/src/error.rs b/crates/rust-mcp-transport/src/error.rs index 3bf6231..55ae29e 100644 --- a/crates/rust-mcp-transport/src/error.rs +++ b/crates/rust-mcp-transport/src/error.rs @@ -5,6 +5,8 @@ use core::fmt; use std::any::Any; use tokio::sync::broadcast; +use crate::utils::CancellationError; + /// A wrapper around a broadcast send error. This structure allows for generic error handling /// by boxing the underlying error into a type-erased form. #[derive(Debug)] @@ -79,6 +81,8 @@ pub type TransportResult = core::result::Result; #[derive(Debug, Error)] pub enum TransportError { + #[error("{0}")] + InvalidOptions(String), #[error("{0}")] SendError(#[from] GenericSendError), #[error("{0}")] @@ -95,4 +99,12 @@ pub enum TransportError { FromString(String), #[error("{0}")] OneshotRecvError(#[from] tokio::sync::oneshot::error::RecvError), + #[error("{0}")] + SendMessageError(#[from] reqwest::Error), + #[error("Http Error: {0}")] + HttpError(u16), + #[error("Shutdown timed out")] + ShutdownTimeout, + #[error("Cancellation error : {0}")] + CancellationError(#[from] CancellationError), } diff --git a/crates/rust-mcp-transport/src/lib.rs b/crates/rust-mcp-transport/src/lib.rs index f229783..a2ec12b 100644 --- a/crates/rust-mcp-transport/src/lib.rs +++ b/crates/rust-mcp-transport/src/lib.rs @@ -2,13 +2,17 @@ // Licensed under the MIT License. See LICENSE file for details. // Modifications to this file must be documented with a description of the changes made. +mod client_sse; pub mod error; mod mcp_stream; mod message_dispatcher; +mod sse; mod stdio; mod transport; mod utils; +pub use client_sse::*; pub use message_dispatcher::*; +pub use sse::*; pub use stdio::*; pub use transport::*; diff --git a/crates/rust-mcp-transport/src/mcp_stream.rs b/crates/rust-mcp-transport/src/mcp_stream.rs index 9ecd6e6..56bdc64 100644 --- a/crates/rust-mcp-transport/src/mcp_stream.rs +++ b/crates/rust-mcp-transport/src/mcp_stream.rs @@ -1,6 +1,7 @@ use crate::{ error::{GenericSendError, TransportError}, message_dispatcher::MessageDispatcher, + utils::CancellationToken, IoStream, }; use futures::Stream; @@ -11,11 +12,11 @@ use std::{ sync::{atomic::AtomicI64, Arc}, time::Duration, }; +use tokio::task::JoinHandle; use tokio::{ io::{AsyncBufReadExt, BufReader}, sync::{broadcast::Sender, oneshot, Mutex}, }; -use tokio::{sync::watch::Receiver, task::JoinHandle}; const CHANNEL_CAPACITY: usize = 36; @@ -36,7 +37,7 @@ impl MCPStream { error_io: IoStream, pending_requests: Arc>>>, request_timeout: Duration, - shutdown_rx: Receiver, + cancellation_token: CancellationToken, ) -> ( Pin + Send>>, MessageDispatcher, @@ -47,8 +48,11 @@ impl MCPStream { { let (tx, rx) = tokio::sync::broadcast::channel::(CHANNEL_CAPACITY); + // Clone cancellation_token for reader + let reader_token = cancellation_token.clone(); + #[allow(clippy::let_underscore_future)] - let _ = Self::spawn_reader(readable, tx, pending_requests.clone(), shutdown_rx); + let _ = Self::spawn_reader(readable, tx, pending_requests.clone(), reader_token); let stream = { Box::pin(futures::stream::unfold(rx, |mut rx| async move { @@ -77,7 +81,7 @@ impl MCPStream { readable: Pin>, tx: Sender, pending_requests: Arc>>>, - mut shutdown_rx: Receiver, + cancellation_token: CancellationToken, ) -> JoinHandle> where R: RpcMessage + Clone + Send + Sync + serde::de::DeserializeOwned + 'static, @@ -87,11 +91,10 @@ impl MCPStream { loop { tokio::select! { - _ = shutdown_rx.changed() =>{ - if *shutdown_rx.borrow() { + _ = cancellation_token.cancelled() => + { break; - } - } + }, line = lines_stream.next_line() =>{ match line { @@ -142,7 +145,6 @@ impl MCPStream { } } } - Ok::<(), TransportError>(()) }) } diff --git a/crates/rust-mcp-transport/src/sse.rs b/crates/rust-mcp-transport/src/sse.rs new file mode 100644 index 0000000..4d8b100 --- /dev/null +++ b/crates/rust-mcp-transport/src/sse.rs @@ -0,0 +1,133 @@ +use async_trait::async_trait; +use futures::Stream; +use rust_mcp_schema::schema_utils::{McpMessage, RpcMessage}; +use rust_mcp_schema::RequestId; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use tokio::io::DuplexStream; +use tokio::sync::Mutex; + +use crate::error::{TransportError, TransportResult}; +use crate::mcp_stream::MCPStream; +use crate::message_dispatcher::MessageDispatcher; +use crate::transport::Transport; +use crate::utils::CancellationTokenSource; +use crate::{IoStream, McpDispatch, TransportOptions}; + +pub struct SseTransport { + shutdown_source: tokio::sync::RwLock>, + is_shut_down: Mutex, + read_write_streams: Mutex>, + options: Arc, +} + +/// Server-Sent Events (SSE) transport implementation +impl SseTransport { + /// Creates a new SseTransport instance + /// + /// Initializes the transport with provided read and write duplex streams and options. + /// + /// # Arguments + /// * `read_rx` - Duplex stream for receiving messages + /// * `write_tx` - Duplex stream for sending messages + /// * `options` - Shared transport configuration options + /// + /// # Returns + /// * `TransportResult` - The initialized transport or an error + pub fn new( + read_rx: DuplexStream, + write_tx: DuplexStream, + options: Arc, + ) -> TransportResult { + Ok(Self { + read_write_streams: Mutex::new(Some((read_rx, write_tx))), + options, + shutdown_source: tokio::sync::RwLock::new(None), + is_shut_down: Mutex::new(false), + }) + } +} + +#[async_trait] +impl Transport for SseTransport +where + R: RpcMessage + Clone + Send + Sync + serde::de::DeserializeOwned + 'static, + S: McpMessage + Clone + Send + Sync + serde::Serialize + 'static, +{ + /// Starts the transport, initializing streams and message dispatcher + /// + /// Sets up the MCP stream and dispatcher using the provided duplex streams. + /// + /// # Returns + /// * `TransportResult<(Pin + Send>>, MessageDispatcher, IoStream)>` + /// - The message stream, dispatcher, and error stream + /// + /// # Errors + /// * Returns `TransportError` if streams are already taken or not initialized + async fn start( + &self, + ) -> TransportResult<( + Pin + Send>>, + MessageDispatcher, + IoStream, + )> + where + MessageDispatcher: McpDispatch, + { + // Create CancellationTokenSource and token + let (cancellation_source, cancellation_token) = CancellationTokenSource::new(); + let mut lock = self.shutdown_source.write().await; + *lock = Some(cancellation_source); + + let pending_requests: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); + + let mut lock = self.read_write_streams.lock().await; + let (read_rx, write_tx) = lock.take().ok_or_else(|| { + TransportError::FromString( + "SSE streams already taken or transport not initialized".to_string(), + ) + })?; + + let (stream, sender, error_stream) = MCPStream::create( + Box::pin(read_rx), + Mutex::new(Box::pin(write_tx)), + IoStream::Writable(Box::pin(tokio::io::stderr())), + pending_requests, + self.options.timeout, + cancellation_token, + ); + + Ok((stream, sender, error_stream)) + } + + /// Checks if the transport has been shut down + /// + /// # Returns + /// * `bool` - True if the transport is shut down, false otherwise + async fn is_shut_down(&self) -> bool { + let result = self.is_shut_down.lock().await; + *result + } + + /// Shuts down the transport, terminating tasks and signaling closure + /// + /// Cancels any running tasks and clears the cancellation source. + /// + /// # Returns + /// * `TransportResult<()>` - Ok if shutdown is successful, Err if cancellation fails + async fn shut_down(&self) -> TransportResult<()> { + // Trigger cancellation + let mut cancellation_lock = self.shutdown_source.write().await; + if let Some(source) = cancellation_lock.as_ref() { + source.cancel()?; + } + *cancellation_lock = None; // Clear cancellation_source + + // Mark as shut down + let mut is_shut_down_lock = self.is_shut_down.lock().await; + *is_shut_down_lock = true; + Ok(()) + } +} diff --git a/crates/rust-mcp-transport/src/stdio.rs b/crates/rust-mcp-transport/src/stdio.rs index 6f2eb62..b5bf5ed 100644 --- a/crates/rust-mcp-transport/src/stdio.rs +++ b/crates/rust-mcp-transport/src/stdio.rs @@ -6,13 +6,13 @@ use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::process::Command; -use tokio::sync::watch::Sender; -use tokio::sync::{watch, Mutex}; +use tokio::sync::Mutex; -use crate::error::{GenericWatchSendError, TransportError, TransportResult}; +use crate::error::{TransportError, TransportResult}; use crate::mcp_stream::MCPStream; use crate::message_dispatcher::MessageDispatcher; use crate::transport::Transport; +use crate::utils::CancellationTokenSource; use crate::{IoStream, McpDispatch, TransportOptions}; /// Implements a standard I/O transport for MCP communication. @@ -27,7 +27,7 @@ pub struct StdioTransport { args: Option>, env: Option>, options: TransportOptions, - shutdown_tx: tokio::sync::RwLock>>, + shutdown_source: tokio::sync::RwLock>, is_shut_down: Mutex, } @@ -51,7 +51,7 @@ impl StdioTransport { command: None, env: None, options, - shutdown_tx: tokio::sync::RwLock::new(None), + shutdown_source: tokio::sync::RwLock::new(None), is_shut_down: Mutex::new(false), }) } @@ -82,7 +82,7 @@ impl StdioTransport { command: Some(command.into()), env, options, - shutdown_tx: tokio::sync::RwLock::new(None), + shutdown_source: tokio::sync::RwLock::new(None), is_shut_down: Mutex::new(false), }) } @@ -140,10 +140,10 @@ where where MessageDispatcher: McpDispatch, { - let (shutdown_tx, shutdown_rx) = watch::channel(false); - - let mut lock = self.shutdown_tx.write().await; - *lock = Some(shutdown_tx); + // Create CancellationTokenSource and token + let (cancellation_source, cancellation_token) = CancellationTokenSource::new(); + let mut lock = self.shutdown_source.write().await; + *lock = Some(cancellation_source); if self.command.is_some() { let (command_name, command_args) = self.launch_commands(); @@ -197,7 +197,7 @@ where IoStream::Readable(Box::pin(stderr)), pending_requests_clone, self.options.timeout, - shutdown_rx, + cancellation_token, ); Ok((stream, sender, error_stream)) @@ -210,7 +210,7 @@ where IoStream::Writable(Box::pin(tokio::io::stderr())), pending_requests, self.options.timeout, - shutdown_rx, + cancellation_token, ); Ok((stream, sender, error_stream)) @@ -233,12 +233,16 @@ where /// # Errors /// Returns a `TransportError` if the shutdown signal fails or the process cannot be killed. async fn shut_down(&self) -> TransportResult<()> { - let lock = self.shutdown_tx.write().await; - if let Some(tx) = lock.as_ref() { - tx.send(true).map_err(GenericWatchSendError::new)?; - let mut lock = self.is_shut_down.lock().await; - *lock = true + // Trigger cancellation + let mut cancellation_lock = self.shutdown_source.write().await; + if let Some(source) = cancellation_lock.as_ref() { + source.cancel()?; } + *cancellation_lock = None; // Clear cancellation_source + + // Mark as shut down + let mut is_shut_down_lock = self.is_shut_down.lock().await; + *is_shut_down_lock = true; Ok(()) } } diff --git a/crates/rust-mcp-transport/src/transport.rs b/crates/rust-mcp-transport/src/transport.rs index 3efc840..ca570aa 100644 --- a/crates/rust-mcp-transport/src/transport.rs +++ b/crates/rust-mcp-transport/src/transport.rs @@ -24,6 +24,7 @@ pub enum IoStream { } /// Configuration for the transport layer +#[derive(Debug, Clone)] pub struct TransportOptions { /// The timeout in milliseconds for requests. /// diff --git a/crates/rust-mcp-transport/src/utils.rs b/crates/rust-mcp-transport/src/utils.rs index 8ffa201..68bc604 100644 --- a/crates/rust-mcp-transport/src/utils.rs +++ b/crates/rust-mcp-transport/src/utils.rs @@ -1,3 +1,15 @@ +mod cancellation_token; +mod http_utils; +mod readable_channel; +mod sse_stream; +mod writable_channel; + +pub(crate) use cancellation_token::*; +pub(crate) use http_utils::*; +pub(crate) use readable_channel::*; +pub(crate) use sse_stream::*; +pub(crate) use writable_channel::*; + use rust_mcp_schema::schema_utils::SdkError; use tokio::time::{timeout, Duration}; @@ -13,3 +25,58 @@ where Err(_) => Err(SdkError::request_timeout(timeout_duration.as_millis()).into()), // Timeout error } } + +pub fn extract_origin(url: &str) -> Option { + // Remove the fragment first (everything after '#') + let url = url.split('#').next()?; // Keep only part before `#` + + // Split scheme and the rest + let (scheme, rest) = url.split_once("://")?; + + // Get host and optionally the port (before first '/') + let end = rest.find('/').unwrap_or(rest.len()); + let host_port = &rest[..end]; + + // Reconstruct origin + Some(format!("{}://{}", scheme, host_port)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_origin_with_path() { + let url = "https://example.com:8080/some/path"; + assert_eq!( + extract_origin(url), + Some("https://example.com:8080".to_string()) + ); + } + + #[test] + fn test_extract_origin_without_path() { + let url = "https://example.com"; + assert_eq!(extract_origin(url), Some("https://example.com".to_string())); + } + + #[test] + fn test_extract_origin_with_fragment() { + let url = "https://example.com:8080/path#section"; + assert_eq!( + extract_origin(url), + Some("https://example.com:8080".to_string()) + ); + } + + #[test] + fn test_extract_origin_invalid_url() { + let url = "example.com/path"; + assert_eq!(extract_origin(url), None); + } + + #[test] + fn test_extract_origin_empty_string() { + assert_eq!(extract_origin(""), None); + } +} diff --git a/crates/rust-mcp-transport/src/utils/cancellation_token.rs b/crates/rust-mcp-transport/src/utils/cancellation_token.rs new file mode 100644 index 0000000..84f5b78 --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/cancellation_token.rs @@ -0,0 +1,256 @@ +use std::sync::Arc; +use tokio::sync::watch; + +/// Error type for cancellation operations +/// +/// Defines possible errors that can occur during cancellation, such as a closed channel. +#[derive(Debug, thiserror::Error)] +pub enum CancellationError { + #[error("Cancellation channel closed")] + ChannelClosed, +} + +/// Token used by tasks to check or await cancellation +/// +/// Holds a receiver for a watch channel to monitor cancellation status. +/// The struct is cloneable to allow multiple tasks to share the same token. +#[derive(Clone)] +pub struct CancellationToken { + receiver: watch::Receiver, +} + +/// Source that controls cancellation +/// +/// Manages the sender side of a watch channel to signal cancellation to associated tokens. +pub struct CancellationTokenSource { + sender: Arc>, +} + +impl CancellationTokenSource { + /// Creates a new CancellationTokenSource and its associated CancellationToken + /// + /// Initializes a watch channel with an initial value of `false` (not cancelled). + /// + /// # Returns + /// * `(Self, CancellationToken)` - A tuple containing the source and its token + pub fn new() -> (Self, CancellationToken) { + let (sender, receiver) = watch::channel(false); + ( + CancellationTokenSource { + sender: Arc::new(sender), + }, + CancellationToken { receiver }, + ) + } + + /// Triggers cancellation + /// + /// Sends a `true` value through the watch channel to signal cancellation to all tokens. + /// + /// # Returns + /// * `Result<(), CancellationError>` - Ok if cancellation is sent, Err if the channel is closed + pub fn cancel(&self) -> Result<(), CancellationError> { + self.sender + .send(true) + .map_err(|_| CancellationError::ChannelClosed) + } + + /// Creates a new CancellationToken linked to this source + /// + /// Subscribes a new receiver to the watch channel for monitoring cancellation. + /// + /// # Returns + /// * `CancellationToken` - A new token linked to this source + #[allow(unused)] + pub fn token(&self) -> CancellationToken { + CancellationToken { + receiver: self.sender.subscribe(), + } + } +} + +impl CancellationToken { + /// Checks if cancellation is requested (non-blocking) + /// + /// # Returns + /// * `bool` - True if cancellation is requested, false otherwise + pub fn is_cancelled(&self) -> bool { + *self.receiver.borrow() + } + + /// Asynchronously waits for cancellation + /// + /// Polls the watch channel until cancellation is signaled or the channel is closed. + /// + /// # Returns + /// * `Result<(), CancellationError>` - Ok if cancellation is received, Err if the channel is closed + pub async fn cancelled(&self) -> Result<(), CancellationError> { + // Clone receiver to avoid mutating the original + let mut receiver = self.receiver.clone(); + // Poll until the value is true or the channel is closed + loop { + if *receiver.borrow() { + return Ok(()); + } + // Wait for any change + receiver + .changed() + .await + .map_err(|_| CancellationError::ChannelClosed)?; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::time::{timeout, Duration}; + + /// Test creating a source and token, verifying initial state + #[tokio::test] + async fn test_create_and_initial_state() { + let (_source, token) = CancellationTokenSource::new(); + + // Verify initial state is not cancelled + assert!(!token.is_cancelled()); + + // Verify token can be awaited without immediate cancellation + let wait_result = timeout(Duration::from_millis(100), token.cancelled()).await; + assert!( + wait_result.is_err(), + "Expected timeout as cancellation not triggered" + ); + } + + /// Test triggering cancellation and checking status + #[tokio::test] + async fn test_trigger_cancellation() { + let (source, token) = CancellationTokenSource::new(); + + // Trigger cancellation + let cancel_result = source.cancel(); + assert!(cancel_result.is_ok(), "Expected successful cancellation"); + + // Verify token reflects cancelled state + assert!(token.is_cancelled()); + + // Verify cancelled() completes immediately + let wait_result = timeout(Duration::from_millis(100), token.cancelled()).await; + assert!(wait_result.is_ok(), "Expected cancellation to complete"); + assert!( + wait_result.unwrap().is_ok(), + "Expected Ok result from cancelled()" + ); + } + + /// Test awaiting cancellation asynchronously + #[tokio::test] + async fn test_await_cancellation() { + let (source, token) = CancellationTokenSource::new(); + + // Spawn a task to trigger cancellation after a delay + let source_clone = source; + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(50)).await; + let _ = source_clone.cancel(); + }); + + // Await cancellation + let wait_result = timeout(Duration::from_millis(200), token.cancelled()).await; + assert!(wait_result.is_ok(), "Expected cancellation within timeout"); + assert!( + wait_result.unwrap().is_ok(), + "Expected Ok result from cancelled()" + ); + assert!(token.is_cancelled(), "Expected token to be cancelled"); + } + + /// Test multiple tokens receiving cancellation + #[tokio::test] + async fn test_multiple_tokens() { + let (source, token1) = CancellationTokenSource::new(); + let token2 = source.token(); + let token3 = source.token(); + + // Verify all tokens start non-cancelled + assert!(!token1.is_cancelled()); + assert!(!token2.is_cancelled()); + assert!(!token3.is_cancelled()); + + // Trigger cancellation + source.cancel().expect("Failed to cancel"); + + // Verify all tokens reflect cancelled state + assert!(token1.is_cancelled()); + assert!(token2.is_cancelled()); + assert!(token3.is_cancelled()); + + // Verify all tokens can await cancellation + let wait1 = timeout(Duration::from_millis(100), token1.cancelled()).await; + let wait2 = timeout(Duration::from_millis(100), token2.cancelled()).await; + let wait3 = timeout(Duration::from_millis(100), token3.cancelled()).await; + + assert!( + wait1.is_ok() && wait1.unwrap().is_ok(), + "Token1 should complete cancellation" + ); + assert!( + wait2.is_ok() && wait2.unwrap().is_ok(), + "Token2 should complete cancellation" + ); + assert!( + wait3.is_ok() && wait3.unwrap().is_ok(), + "Token3 should complete cancellation" + ); + } + + /// Test channel closure by dropping the source + #[tokio::test] + async fn test_channel_closed_error() { + let (source, token) = CancellationTokenSource::new(); + + // Drop the source to close the channel + drop(source); + + // Attempt to await cancellation + let wait_result = token.cancelled().await; + assert!( + matches!(wait_result, Err(CancellationError::ChannelClosed)), + "Expected ChannelClosed error" + ); + + // Verify token still reports non-cancelled (no signal received) + assert!(!token.is_cancelled()); + } + + /// Test creating a new token with the token() method + #[tokio::test] + async fn test_new_token_creation() { + let (source, token1) = CancellationTokenSource::new(); + let token2 = source.token(); + + // Verify both tokens start non-cancelled + assert!(!token1.is_cancelled()); + assert!(!token2.is_cancelled()); + + // Trigger cancellation + source.cancel().expect("Failed to cancel"); + + // Verify both tokens reflect cancelled state + assert!(token1.is_cancelled()); + assert!(token2.is_cancelled()); + + // Verify both tokens can await cancellation + let wait1 = timeout(Duration::from_millis(100), token1.cancelled()).await; + let wait2 = timeout(Duration::from_millis(100), token2.cancelled()).await; + + assert!( + wait1.is_ok() && wait1.unwrap().is_ok(), + "Token1 should complete cancellation" + ); + assert!( + wait2.is_ok() && wait2.unwrap().is_ok(), + "Token2 should complete cancellation" + ); + } +} diff --git a/crates/rust-mcp-transport/src/utils/http_utils.rs b/crates/rust-mcp-transport/src/utils/http_utils.rs new file mode 100644 index 0000000..dc0237a --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/http_utils.rs @@ -0,0 +1,150 @@ +use crate::error::{TransportError, TransportResult}; + +use reqwest::header::{HeaderMap, CONTENT_TYPE}; +use reqwest::Client; + +/// Sends an HTTP POST request with the given body and headers +/// +/// # Arguments +/// * `client` - The HTTP client to use +/// * `post_url` - The URL to send the POST request to +/// * `body` - The JSON body as a string +/// * `headers` - Optional custom headers +/// +/// # Returns +/// * `TransportResult<()>` - Ok if the request is successful, Err otherwise +pub async fn http_post( + client: &Client, + post_url: &str, + body: String, + headers: &Option, +) -> TransportResult<()> { + let mut request = client + .post(post_url) + .header(CONTENT_TYPE, "application/json") + .body(body); + + if let Some(map) = headers { + request = request.headers(map.clone()); + } + let response = request.send().await?; + if !response.status().is_success() { + return Err(TransportError::HttpError(response.status().as_u16())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; + use wiremock::{ + matchers::{body_json_string, header, method, path}, + Mock, MockServer, ResponseTemplate, + }; + + /// Helper function to create custom headers for testing + fn create_test_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_static("x-custom-header"), + HeaderValue::from_static("test-value"), + ); + headers + } + + #[tokio::test] + async fn test_http_post_success() { + // Start a mock server + let mock_server = MockServer::start().await; + + // Mock a successful POST response + Mock::given(method("POST")) + .and(path("/test")) + .and(header("Content-Type", "application/json")) + .and(body_json_string(r#"{"key":"value"}"#)) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; + + let client = Client::new(); + let url = format!("{}/test", mock_server.uri()); + let body = r#"{"key":"value"}"#.to_string(); + let headers = None; + + // Perform the POST request + let result = http_post(&client, &url, body, &headers).await; + + // Assert the result is Ok + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_http_post_non_success_status() { + // Start a mock server + let mock_server = MockServer::start().await; + + // Mock a 400 Bad Request response + Mock::given(method("POST")) + .and(path("/test")) + .and(header("Content-Type", "application/json")) + .respond_with(ResponseTemplate::new(400)) + .mount(&mock_server) + .await; + + let client = Client::new(); + let url = format!("{}/test", mock_server.uri()); + let body = r#"{"key":"value"}"#.to_string(); + let headers = None; + + // Perform the POST request + let result = http_post(&client, &url, body, &headers).await; + + // Assert the result is an HttpError with status 400 + match result { + Err(TransportError::HttpError(status)) => assert_eq!(status, 400), + _ => panic!("Expected HttpError with status 400"), + } + } + + #[tokio::test] + async fn test_http_post_with_custom_headers() { + // Start a mock server + let mock_server = MockServer::start().await; + + // Mock a successful POST response with custom header + Mock::given(method("POST")) + .and(path("/test")) + .and(header("Content-Type", "application/json")) + .and(header("x-custom-header", "test-value")) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; + + let client = Client::new(); + let url = format!("{}/test", mock_server.uri()); + let body = r#"{"key":"value"}"#.to_string(); + let headers = Some(create_test_headers()); + + // Perform the POST request + let result = http_post(&client, &url, body, &headers).await; + + // Assert the result is Ok + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_http_post_network_error() { + // Use an invalid URL to simulate a network error + let client = Client::new(); + let url = "http://localhost:9999/test"; // Assuming no server is running on this port + let body = r#"{"key":"value"}"#.to_string(); + let headers = None; + + // Perform the POST request + let result = http_post(&client, url, body, &headers).await; + + // Assert the result is an error (likely a connection error) + assert!(result.is_err()); + } +} diff --git a/crates/rust-mcp-transport/src/utils/readable_channel.rs b/crates/rust-mcp-transport/src/utils/readable_channel.rs new file mode 100644 index 0000000..d73697f --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/readable_channel.rs @@ -0,0 +1,162 @@ +use bytes::Bytes; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, ErrorKind}; +use tokio::sync::mpsc; + +/// A readable channel for asynchronous byte stream reading +/// +/// Wraps a Tokio mpsc receiver to provide an AsyncRead implementation, +/// buffering incoming data as needed. +pub struct ReadableChannel { + pub read_rx: mpsc::Receiver, + pub buffer: Bytes, +} + +impl AsyncRead for ReadableChannel { + /// Polls the channel for readable data + /// + /// Attempts to fill the provided buffer with data from the internal buffer + /// or the mpsc receiver. Handles partial reads and channel closure. + /// + /// # Arguments + /// * `self` - Pinned mutable reference to the ReadableChannel + /// * `cx` - Task context for polling + /// * `buf` - Read buffer to fill with data + /// + /// # Returns + /// * `Poll>` - Ready with Ok if data is read, Ready with Err if the channel is closed, or Pending if no data is available + + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + // Check if there is data in the internal buffer + if !self.buffer.is_empty() { + let to_copy = std::cmp::min(self.buffer.len(), buf.remaining()); + buf.put_slice(&self.buffer[..to_copy]); + self.buffer = self.buffer.slice(to_copy..); + return Poll::Ready(Ok(())); + } + // Poll the receiver for new data + match Pin::new(&mut self.read_rx).poll_recv(cx) { + Poll::Ready(Some(data)) => { + let to_copy = std::cmp::min(data.len(), buf.remaining()); + buf.put_slice(&data[..to_copy]); + if to_copy < data.len() { + self.buffer = data.slice(to_copy..); + } + + Poll::Ready(Ok(())) + } + Poll::Ready(None) => Poll::Ready(Err(std::io::Error::new( + ErrorKind::BrokenPipe, + "Channel closed", + ))), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use tokio::sync::mpsc; + + use super::ReadableChannel; + use tokio::io::{AsyncReadExt, ErrorKind}; + + #[tokio::test] + async fn test_read_single_message() { + let (tx, rx) = tokio::sync::mpsc::channel(1); + let data = bytes::Bytes::from("hello world"); + tx.send(data.clone()).await.unwrap(); + drop(tx); // close the channel + + let mut reader = super::ReadableChannel { + read_rx: rx, + buffer: bytes::Bytes::new(), + }; + + let mut buf = vec![0; 11]; + reader.read_exact(&mut buf).await.unwrap(); // no need to assign + assert_eq!(buf, data); + } + + #[tokio::test] + async fn test_read_partial_then_continue() { + let (tx, rx) = mpsc::channel(1); + let data = Bytes::from("hello world"); + tx.send(data.clone()).await.unwrap(); + + let mut reader = ReadableChannel { + read_rx: rx, + buffer: Bytes::new(), + }; + + let mut buf1 = vec![0; 5]; + reader.read_exact(&mut buf1).await.unwrap(); + assert_eq!(&buf1, b"hello"); + + let mut buf2 = vec![0; 6]; + reader.read_exact(&mut buf2).await.unwrap(); + assert_eq!(&buf2, b" world"); + } + + #[tokio::test] + async fn test_read_larger_than_buffer() { + let (tx, rx) = mpsc::channel(1); + let data = Bytes::from("abcdefghij"); // 10 bytes + tx.send(data).await.unwrap(); + + let mut reader = ReadableChannel { + read_rx: rx, + buffer: Bytes::new(), + }; + + let mut buf = vec![0; 6]; + reader.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"abcdef"); + + let mut buf2 = vec![0; 4]; + reader.read_exact(&mut buf2).await.unwrap(); + assert_eq!(&buf2, b"ghij"); + } + + #[tokio::test] + async fn test_read_after_channel_closed() { + let (tx, rx) = mpsc::channel(1); + drop(tx); // Close without sending + + let mut reader = ReadableChannel { + read_rx: rx, + buffer: Bytes::new(), + }; + + let mut buf = vec![0; 5]; + let result = reader.read_exact(&mut buf).await; + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BrokenPipe); + } + + #[tokio::test] + async fn test_pending_read() { + use tokio::time::{timeout, Duration}; + + let (_tx, rx) = tokio::sync::mpsc::channel::(1); + + let mut reader = super::ReadableChannel { + read_rx: rx, + buffer: bytes::Bytes::new(), + }; + + let mut buf = vec![0; 5]; + let result = timeout(Duration::from_millis(100), reader.read_exact(&mut buf)).await; + + // If the channel has no data and is still open, read should timeout + assert!(result.is_err()); + } +} diff --git a/crates/rust-mcp-transport/src/utils/sse_stream.rs b/crates/rust-mcp-transport/src/utils/sse_stream.rs new file mode 100644 index 0000000..aac29c8 --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/sse_stream.rs @@ -0,0 +1,180 @@ +use bytes::{Bytes, BytesMut}; +use reqwest::Client; +use std::time::Duration; +use tokio::sync::{mpsc, oneshot}; +use tokio::time; +use tokio_stream::StreamExt; + +use super::CancellationToken; + +const BUFFER_CAPACITY: usize = 1024; +const ENDPOINT_SSE_EVENT: &str = "endpoint"; + +/// Server-Sent Events (SSE) stream handler +/// +/// Manages an SSE connection, handling reconnection logic and streaming data to a channel. +pub(crate) struct SseStream { + /// HTTP client for making SSE requests + pub sse_client: Client, + /// URL of the SSE endpoint + pub sse_url: String, + /// Maximum number of retry attempts for failed connections + pub max_retries: usize, + /// Delay between retry attempts + pub retry_delay: Duration, + /// Sender for transmitting received data to the readable channel + pub read_tx: mpsc::Sender, +} + +impl SseStream { + /// Runs the SSE stream, processing incoming events and handling reconnections + /// + /// Continuously attempts to connect to the SSE endpoint in case connection is lost, processes incoming data, + /// and sends it to the read channel. Handles retries and cancellation. + /// + /// # Arguments + /// * `endpoint_event_tx` - Optional one-shot sender for the messages endpoint + /// * `cancellation_token` - Token for monitoring cancellation requests + pub(crate) async fn run( + &self, + mut endpoint_event_tx: Option>>, + cancellation_token: CancellationToken, + ) { + let mut retry_count = 0; + let mut buffer = BytesMut::with_capacity(BUFFER_CAPACITY); + let mut endpoint_event_received = false; + + // Main loop for reconnection attempts + loop { + // Check for cancellation before attempting connection + if cancellation_token.is_cancelled() { + tracing::info!("SSE cancelled before connection attempt"); + return; + } + + // Send GET request to the SSE endpoint + let response = match self + .sse_client + .get(&self.sse_url) + .header("Accept", "text/event-stream") + .send() + .await + { + Ok(resp) => resp, + Err(e) => { + eprintln!("Failed to connect to SSE: {}", e); + if retry_count >= self.max_retries { + tracing::error!("Max retries reached, giving up"); + if let Some(tx) = endpoint_event_tx.take() { + let _ = tx.send(None); + } + return; + } + retry_count += 1; + time::sleep(self.retry_delay).await; + continue; + } + }; + + // Create a stream from the response bytes + let mut stream = response.bytes_stream(); + + // Inner loop for processing stream chunks + loop { + let next_chunk = tokio::select! { + // Wait for the next stream chunk + chunk = stream.next() => { + match chunk { + Some(chunk) => chunk, + None => break, // Stream ended, break from inner loop to reconnect + } + } + // Wait for cancellation + _ = cancellation_token.cancelled() => { + return; + } + }; + + match next_chunk { + Ok(bytes) => { + buffer.extend_from_slice(&bytes); + + let mut batch = Vec::new(); + // collect complete lines for processing + while let Some(pos) = buffer.iter().position(|&b| b == b'\n') { + let line = buffer.split_to(pos + 1).freeze(); + // Skip empty lines + if line.len() > 1 { + batch.push(line); + } + } + + let mut current_event: Option = None; + + // Process complete lines + for line in batch { + // Parse line as UTF-8, keep the trailing newline + let line_str = String::from_utf8_lossy(&line); + + if let Some(event_name) = line_str.strip_prefix("event: ") { + current_event = Some(event_name.trim().to_string()); + continue; + } + + // Extract content after data: or : + let content = if let Some(data) = line_str.strip_prefix("data: ") { + let payload = data.trim_start(); + if !endpoint_event_received { + if let Some(ENDPOINT_SSE_EVENT) = current_event.as_deref() { + if let Some(tx) = endpoint_event_tx.take() { + endpoint_event_received = true; + let _ = tx.send(Some(payload.trim().to_owned())); + continue; + } + } + } + payload + } else if let Some(comment) = line_str.strip_prefix(":") { + comment.trim_start() + } else { + continue; + }; + + if !content.is_empty() { + let bytes = Bytes::copy_from_slice(content.as_bytes()); + if self.read_tx.send(bytes).await.is_err() { + eprintln!("Readable stream closed, shutting down SSE task"); + if !endpoint_event_received { + if let Some(tx) = endpoint_event_tx.take() { + let _ = tx.send(None); + } + } + return; + } + } + } + retry_count = 0; // Reset retry count on successful chunk + } + Err(e) => { + tracing::error!("SSE stream error: {}", e); + if retry_count >= self.max_retries { + tracing::error!("Max retries reached, giving up"); + if !endpoint_event_received { + if let Some(tx) = endpoint_event_tx.take() { + let _ = tx.send(None); + } + } + return; + } + retry_count += 1; + time::sleep(self.retry_delay).await; + break; // Break inner loop to reconnect + } + } + } + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/crates/rust-mcp-transport/src/utils/writable_channel.rs b/crates/rust-mcp-transport/src/utils/writable_channel.rs new file mode 100644 index 0000000..4b65129 --- /dev/null +++ b/crates/rust-mcp-transport/src/utils/writable_channel.rs @@ -0,0 +1,150 @@ +use bytes::Bytes; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{self, AsyncWrite, ErrorKind}; +use tokio::sync::mpsc; + +/// A writable channel for asynchronous byte stream writing +/// +/// Wraps a Tokio mpsc sender to provide an AsyncWrite implementation, +/// enabling asynchronous writing of byte data to a channel. +pub(crate) struct WritableChannel { + pub write_tx: mpsc::Sender, +} + +impl AsyncWrite for WritableChannel { + /// Polls the channel to write data + /// + /// Attempts to send the provided buffer through the mpsc channel. + /// If the channel is full, spawns a task to send the data asynchronously + /// and notifies the waker upon completion. + /// + /// # Arguments + /// * `self` - Pinned mutable reference to the WritableChannel + /// * `cx` - Task context for polling + /// * `buf` - Data buffer to write + /// + /// # Returns + /// * `Poll>` - Ready with the number of bytes written if successful, + /// Ready with an error if the channel is closed, or Pending if the channel is full + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let write_tx = self.write_tx.clone(); + let bytes = Bytes::copy_from_slice(buf); + match write_tx.try_send(bytes.clone()) { + Ok(()) => Poll::Ready(Ok(buf.len())), + Err(mpsc::error::TrySendError::Full(_)) => { + let waker = cx.waker().clone(); + tokio::spawn(async move { + if write_tx.send(bytes).await.is_ok() { + waker.wake(); + } + }); + Poll::Pending + } + Err(mpsc::error::TrySendError::Closed(_)) => Poll::Ready(Err(std::io::Error::new( + ErrorKind::BrokenPipe, + "Channel closed", + ))), + } + } + /// Polls to flush the channel + /// + /// Since the channel does not buffer data internally, this is a no-op. + /// + /// # Arguments + /// * `self` - Pinned mutable reference to the WritableChannel + /// * `_cx` - Task context for polling (unused) + /// + /// # Returns + /// * `Poll>` - Always Ready with Ok + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + /// Polls to shut down the channel + /// + /// Since the channel does not require explicit shutdown, this is a no-op. + /// + /// # Arguments + /// * `self` - Pinned mutable reference to the WritableChannel + /// * `_cx` - Task context for polling (unused) + /// + /// # Returns + /// * `Poll>` - Always Ready with Ok + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod tests { + use super::WritableChannel; + use bytes::Bytes; + use tokio::io::{AsyncWriteExt, ErrorKind}; + use tokio::sync::mpsc; + + #[tokio::test] + async fn test_write_successful() { + let (tx, mut rx) = mpsc::channel(1); + let mut writer = WritableChannel { write_tx: tx }; + + let data = b"hello world"; + let n = writer.write(data).await.unwrap(); + assert_eq!(n, data.len()); + + let received = rx.recv().await.unwrap(); + assert_eq!(received, Bytes::from_static(data)); + } + + #[tokio::test] + async fn test_write_when_channel_full() { + let (tx, mut rx) = mpsc::channel(1); + + // Pre-fill the channel to make it full + tx.send(Bytes::from_static(b"pre-filled")).await.unwrap(); + + let mut writer = WritableChannel { write_tx: tx }; + + let data = b"deferred"; + + // Start the write, which will hit "Full" and spawn a task + let write_future = writer.write(data); + + // Drain the channel to make space + let _ = rx.recv().await; + + // Await the write now that there's space + let n = write_future.await.unwrap(); + assert_eq!(n, data.len()); + + let received = rx.recv().await.unwrap(); + assert_eq!(received, Bytes::from_static(data)); + } + + #[tokio::test] + async fn test_write_after_channel_closed() { + let (tx, rx) = mpsc::channel(1); + drop(rx); // simulate the receiver being dropped (channel is closed) + + let mut writer = WritableChannel { write_tx: tx }; + + let result = writer.write(b"data").await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BrokenPipe); + } + + #[tokio::test] + async fn test_poll_flush_and_shutdown() { + let (tx, _rx) = mpsc::channel(1); + let mut writer = WritableChannel { write_tx: tx }; + + // These are no-ops, just ensure they return Ok + writer.flush().await.unwrap(); + writer.shutdown().await.unwrap(); + } +} diff --git a/examples/hello-world-mcp-server-core/Cargo.toml b/examples/hello-world-mcp-server-core/Cargo.toml index a78a4c8..fae4f20 100644 --- a/examples/hello-world-mcp-server-core/Cargo.toml +++ b/examples/hello-world-mcp-server-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-world-mcp-server-core" -version = "0.1.9" +version = "0.1.0" edition = "2021" publish = false license = "MIT" diff --git a/examples/hello-world-mcp-server/Cargo.toml b/examples/hello-world-mcp-server/Cargo.toml index d92b841..373ab01 100644 --- a/examples/hello-world-mcp-server/Cargo.toml +++ b/examples/hello-world-mcp-server/Cargo.toml @@ -10,6 +10,8 @@ license = "MIT" rust-mcp-sdk = { workspace = true, default-features = false, features = [ "server", "macros", + "hyper-server", + "ssl", ] } rust-mcp-schema = { workspace = true } @@ -18,6 +20,9 @@ serde = { workspace = true } serde_json = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + [lints] workspace = true diff --git a/examples/hello-world-mcp-server/src/main.rs b/examples/hello-world-mcp-server/src/main.rs index 6785459..41364f5 100644 --- a/examples/hello-world-mcp-server/src/main.rs +++ b/examples/hello-world-mcp-server/src/main.rs @@ -36,7 +36,6 @@ async fn main() -> SdkResult<()> { let transport = StdioTransport::new(TransportOptions::default())?; // STEP 3: instantiate our custom handler for handling MCP messages - let handler = MyServerHandler {}; // STEP 4: create a MCP server diff --git a/examples/hello-world-server-core-sse/.gitignore b/examples/hello-world-server-core-sse/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/hello-world-server-core-sse/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/hello-world-server-core-sse/Cargo.toml b/examples/hello-world-server-core-sse/Cargo.toml new file mode 100644 index 0000000..d2f9c9d --- /dev/null +++ b/examples/hello-world-server-core-sse/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "hello-world-server-core-sse" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT" + + +[dependencies] +rust-mcp-sdk = { workspace = true, default-features = false, features = [ + "server", + "macros", + "hyper-server", +] } +rust-mcp-schema = { workspace = true } + +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + + +[lints] +workspace = true diff --git a/examples/hello-world-server-core-sse/README.md b/examples/hello-world-server-core-sse/README.md new file mode 100644 index 0000000..2dd1fbe --- /dev/null +++ b/examples/hello-world-server-core-sse/README.md @@ -0,0 +1,34 @@ +# Hello World MCP Server (Core) - SSE Transport + +A basic MCP server implementation featuring two custom tools: `Say Hello` and `Say Goodbye` , utilizing [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) and [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) , using SSE transport + +## Overview + +This project showcases a fundamental MCP server implementation, highlighting the capabilities of +[rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) and [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) and with these features: + +- SSE transport +- Custom server handler +- Basic server capabilities + +## Running the Example + +1. Clone the repository: + +```bash +git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git +cd rust-mcp-sdk +``` + +2. Build and start the server: + +```bash +cargo run -p hello-world-server-sse --release +``` + +By default, the SSE endpoint is accessible at `http://127.0.0.1:8080/sse`. +You can test it with [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector), or alternatively, use it with any MCP client you prefer. + +Here you can see it in action : + +![hello-world-mcp-server-sse-core](../../assets/examples/hello-world-server-sse.gif) diff --git a/examples/hello-world-server-core-sse/src/handler.rs b/examples/hello-world-server-core-sse/src/handler.rs new file mode 100644 index 0000000..af8daa5 --- /dev/null +++ b/examples/hello-world-server-core-sse/src/handler.rs @@ -0,0 +1,90 @@ +use async_trait::async_trait; + +use rust_mcp_schema::{ + schema_utils::{CallToolError, NotificationFromClient, RequestFromClient, ResultFromServer}, + ClientRequest, ListToolsResult, RpcError, +}; +use rust_mcp_sdk::{mcp_server::ServerHandlerCore, McpServer}; + +use crate::tools::GreetingTools; + +pub struct MyServerHandler; + +// To check out a list of all the methods in the trait that you can override, take a look at +// https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler_core.rs +#[allow(unused)] +#[async_trait] +impl ServerHandlerCore for MyServerHandler { + // Process incoming requests from the client + async fn handle_request( + &self, + request: RequestFromClient, + runtime: &dyn McpServer, + ) -> std::result::Result { + let method_name = &request.method().to_owned(); + match request { + //Handle client requests according to their specific type. + RequestFromClient::ClientRequest(client_request) => match client_request { + // Handle the initialization request + ClientRequest::InitializeRequest(_) => Ok(runtime.server_info().to_owned().into()), + + // Handle ListToolsRequest, return list of available tools + ClientRequest::ListToolsRequest(_) => Ok(ListToolsResult { + meta: None, + next_cursor: None, + tools: GreetingTools::tools(), + } + .into()), + + // Handles incoming CallToolRequest and processes it using the appropriate tool. + ClientRequest::CallToolRequest(request) => { + let tool_name = request.tool_name().to_string(); + + // Attempt to convert request parameters into GreetingTools enum + let tool_params = GreetingTools::try_from(request.params) + .map_err(|_| CallToolError::unknown_tool(tool_name.clone()))?; + + // Match the tool variant and execute its corresponding logic + let result = match tool_params { + GreetingTools::SayHelloTool(say_hello_tool) => { + say_hello_tool.call_tool().map_err(|err| { + RpcError::internal_error().with_message(err.to_string()) + })? + } + GreetingTools::SayGoodbyeTool(say_goodbye_tool) => { + say_goodbye_tool.call_tool().map_err(|err| { + RpcError::internal_error().with_message(err.to_string()) + })? + } + }; + Ok(result.into()) + } + + // Return Method not found for any other requests + _ => Err(RpcError::method_not_found() + .with_message(format!("No handler is implemented for '{}'.", method_name,))), + }, + // Handle custom requests + RequestFromClient::CustomRequest(_) => Err(RpcError::method_not_found() + .with_message("No handler is implemented for custom requests.".to_string())), + } + } + + // Process incoming client notifications + async fn handle_notification( + &self, + notification: NotificationFromClient, + _: &dyn McpServer, + ) -> std::result::Result<(), RpcError> { + Ok(()) + } + + // Process incoming client errors + async fn handle_error( + &self, + error: RpcError, + _: &dyn McpServer, + ) -> std::result::Result<(), RpcError> { + Ok(()) + } +} diff --git a/examples/hello-world-server-core-sse/src/main.rs b/examples/hello-world-server-core-sse/src/main.rs new file mode 100644 index 0000000..6a44f7a --- /dev/null +++ b/examples/hello-world-server-core-sse/src/main.rs @@ -0,0 +1,52 @@ +mod handler; +mod tools; + +use handler::MyServerHandler; +use rust_mcp_schema::{ + Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, + LATEST_PROTOCOL_VERSION, +}; +use rust_mcp_sdk::{ + error::SdkResult, + mcp_server::{hyper_server_core, HyperServerOptions}, +}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() -> SdkResult<()> { + // initialize tracing + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + // STEP 1: Define server details and capabilities + let server_details = InitializeResult { + // server name and version + server_info: Implementation { + name: "Hello World MCP Server SSE".to_string(), + version: "0.1.0".to_string(), + }, + capabilities: ServerCapabilities { + // indicates that server support mcp tools + tools: Some(ServerCapabilitiesTools { list_changed: None }), + ..Default::default() // Using default values for other fields + }, + meta: None, + instructions: Some("server instructions...".to_string()), + protocol_version: LATEST_PROTOCOL_VERSION.to_string(), + }; + + // STEP 2: instantiate our custom handler for handling MCP messages + let handler = MyServerHandler {}; + + // STEP 3: create a MCP server + let server = + hyper_server_core::create_server(server_details, handler, HyperServerOptions::default()); + + // STEP 4: Start the server + server.start().await?; + Ok(()) +} diff --git a/examples/hello-world-server-core-sse/src/tools.rs b/examples/hello-world-server-core-sse/src/tools.rs new file mode 100644 index 0000000..26a89cb --- /dev/null +++ b/examples/hello-world-server-core-sse/src/tools.rs @@ -0,0 +1,50 @@ +use rust_mcp_schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::{ + macros::{mcp_tool, JsonSchema}, + tool_box, +}; + +//****************// +// SayHelloTool // +//****************// +#[mcp_tool( + name = "say_hello", + description = "Accepts a person's name and says a personalized \"Hello\" to that person" +)] +#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, JsonSchema)] +pub struct SayHelloTool { + /// The name of the person to greet with a "Hello". + name: String, +} + +impl SayHelloTool { + pub fn call_tool(&self) -> Result { + let hello_message = format!("Hello, {}!", self.name); + Ok(CallToolResult::text_content(hello_message, None)) + } +} + +//******************// +// SayGoodbyeTool // +//******************// +#[mcp_tool( + name = "say_goodbye", + description = "Accepts a person's name and says a personalized \"Goodbye\" to that person." +)] +#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, JsonSchema)] +pub struct SayGoodbyeTool { + /// The name of the person to say goodbye to. + name: String, +} +impl SayGoodbyeTool { + pub fn call_tool(&self) -> Result { + let hello_message = format!("Goodbye, {}!", self.name); + Ok(CallToolResult::text_content(hello_message, None)) + } +} + +//******************// +// GreetingTools // +//******************// +// Generates an enum names GreetingTools, with SayHelloTool and SayGoodbyeTool variants +tool_box!(GreetingTools, [SayHelloTool, SayGoodbyeTool]); diff --git a/examples/hello-world-server-sse/Cargo.toml b/examples/hello-world-server-sse/Cargo.toml new file mode 100644 index 0000000..aa08180 --- /dev/null +++ b/examples/hello-world-server-sse/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "hello-world-server-sse" +version = "0.1.9" +edition = "2021" +publish = false +license = "MIT" + + +[dependencies] +rust-mcp-sdk = { workspace = true, default-features = false, features = [ + "server", + "macros", + "hyper-server", +] } +rust-mcp-schema = { workspace = true } + +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + + +[lints] +workspace = true diff --git a/examples/hello-world-server-sse/README.md b/examples/hello-world-server-sse/README.md new file mode 100644 index 0000000..463c04b --- /dev/null +++ b/examples/hello-world-server-sse/README.md @@ -0,0 +1,33 @@ +# Hello World MCP Server - SSE Transport + +A basic MCP server implementation using SSE transport, featuring two custom tools: `Say Hello` and `Say Goodbye` , utilizing [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) and [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) , using SSE transport + +## Overview + +This project showcases a fundamental MCP server implementation, highlighting the capabilities of [rust-mcp-sdk](https://github.com/rust-mcp-stack/rust-mcp-sdk) and [rust-mcp-schema](https://github.com/rust-mcp-stack/rust-mcp-schema) with these features: + +- SSE transport +- Custom server handler +- Basic server capabilities + +## Running the Example + +1. Clone the repository: + +```bash +git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git +cd rust-mcp-sdk +``` + +2. Build and start the server: + +```bash +cargo run -p hello-world-server-sse --release +``` + +By default, the SSE endpoint is accessible at `http://127.0.0.1:8080/sse`. +You can test it with [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector), or alternatively, use it with any MCP client you prefer. + +Here you can see it in action : + +![hello-world-mcp-server](../../assets/examples/hello-world-server-sse.gif) diff --git a/examples/hello-world-server-sse/src/handler.rs b/examples/hello-world-server-sse/src/handler.rs new file mode 100644 index 0000000..f24ba84 --- /dev/null +++ b/examples/hello-world-server-sse/src/handler.rs @@ -0,0 +1,50 @@ +use async_trait::async_trait; +use rust_mcp_schema::{ + schema_utils::CallToolError, CallToolRequest, CallToolResult, ListToolsRequest, + ListToolsResult, RpcError, +}; +use rust_mcp_sdk::{mcp_server::ServerHandler, McpServer}; + +use crate::tools::GreetingTools; + +// Custom Handler to handle MCP Messages +pub struct MyServerHandler; + +// To check out a list of all the methods in the trait that you can override, take a look at +// https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs + +#[async_trait] +#[allow(unused)] +impl ServerHandler for MyServerHandler { + // Handle ListToolsRequest, return list of available tools as ListToolsResult + async fn handle_list_tools_request( + &self, + request: ListToolsRequest, + runtime: &dyn McpServer, + ) -> std::result::Result { + Ok(ListToolsResult { + meta: None, + next_cursor: None, + tools: GreetingTools::tools(), + }) + } + + /// Handles incoming CallToolRequest and processes it using the appropriate tool. + async fn handle_call_tool_request( + &self, + request: CallToolRequest, + runtime: &dyn McpServer, + ) -> std::result::Result { + // Attempt to convert request parameters into GreetingTools enum + let tool_params: GreetingTools = + GreetingTools::try_from(request.params).map_err(CallToolError::new)?; + + // Match the tool variant and execute its corresponding logic + match tool_params { + GreetingTools::SayHelloTool(say_hello_tool) => say_hello_tool.call_tool(), + GreetingTools::SayGoodbyeTool(say_goodbye_tool) => say_goodbye_tool.call_tool(), + } + } + + async fn on_server_started(&self, runtime: &dyn McpServer) {} +} diff --git a/examples/hello-world-server-sse/src/main.rs b/examples/hello-world-server-sse/src/main.rs new file mode 100644 index 0000000..dbf9489 --- /dev/null +++ b/examples/hello-world-server-sse/src/main.rs @@ -0,0 +1,70 @@ +mod handler; +mod tools; + +use std::time::Duration; + +use rust_mcp_sdk::mcp_server::{hyper_server, HyperServerOptions}; + +use handler::MyServerHandler; +use rust_mcp_schema::{ + Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, + LATEST_PROTOCOL_VERSION, +}; + +use rust_mcp_sdk::{error::SdkResult, mcp_server::ServerHandler}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +pub struct AppStateX { + pub server_details: InitializeResult, + pub handler: H, +} + +#[tokio::main] +async fn main() -> SdkResult<()> { + // initialize tracing + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // STEP 1: Define server details and capabilities + let server_details = InitializeResult { + // server name and version + server_info: Implementation { + name: "Hello World MCP Server SSE".to_string(), + version: "0.1.0".to_string(), + }, + capabilities: ServerCapabilities { + // indicates that server support mcp tools + tools: Some(ServerCapabilitiesTools { list_changed: None }), + ..Default::default() // Using default values for other fields + }, + meta: None, + instructions: Some("server instructions...".to_string()), + protocol_version: LATEST_PROTOCOL_VERSION.to_string(), + }; + + // STEP 2: instantiate our custom handler for handling MCP messages + let handler = MyServerHandler {}; + + // STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions + let server = hyper_server::create_server( + server_details, + handler, + HyperServerOptions { + host: "127.0.0.1".to_string(), + ping_interval: Duration::from_secs(5), + ..Default::default() + }, + ); + + // tracing::info!("{}", server.server_info(None).await?); + + // STEP 4: Start the server + server.start().await?; + + Ok(()) +} diff --git a/examples/hello-world-server-sse/src/tools.rs b/examples/hello-world-server-sse/src/tools.rs new file mode 100644 index 0000000..26a89cb --- /dev/null +++ b/examples/hello-world-server-sse/src/tools.rs @@ -0,0 +1,50 @@ +use rust_mcp_schema::{schema_utils::CallToolError, CallToolResult}; +use rust_mcp_sdk::{ + macros::{mcp_tool, JsonSchema}, + tool_box, +}; + +//****************// +// SayHelloTool // +//****************// +#[mcp_tool( + name = "say_hello", + description = "Accepts a person's name and says a personalized \"Hello\" to that person" +)] +#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, JsonSchema)] +pub struct SayHelloTool { + /// The name of the person to greet with a "Hello". + name: String, +} + +impl SayHelloTool { + pub fn call_tool(&self) -> Result { + let hello_message = format!("Hello, {}!", self.name); + Ok(CallToolResult::text_content(hello_message, None)) + } +} + +//******************// +// SayGoodbyeTool // +//******************// +#[mcp_tool( + name = "say_goodbye", + description = "Accepts a person's name and says a personalized \"Goodbye\" to that person." +)] +#[derive(Debug, ::serde::Deserialize, ::serde::Serialize, JsonSchema)] +pub struct SayGoodbyeTool { + /// The name of the person to say goodbye to. + name: String, +} +impl SayGoodbyeTool { + pub fn call_tool(&self) -> Result { + let hello_message = format!("Goodbye, {}!", self.name); + Ok(CallToolResult::text_content(hello_message, None)) + } +} + +//******************// +// GreetingTools // +//******************// +// Generates an enum names GreetingTools, with SayHelloTool and SayGoodbyeTool variants +tool_box!(GreetingTools, [SayHelloTool, SayGoodbyeTool]); diff --git a/examples/simple-mcp-client-core-sse/Cargo.toml b/examples/simple-mcp-client-core-sse/Cargo.toml new file mode 100644 index 0000000..5653278 --- /dev/null +++ b/examples/simple-mcp-client-core-sse/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "simple-mcp-client-core-sse" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT" + + +[dependencies] +rust-mcp-sdk = { workspace = true, default-features = false, features = [ + "client", + "macros", +] } +rust-mcp-schema = { workspace = true } + +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +colored = "3.0.0" +tracing-subscriber = { workspace = true } +tracing = { workspace = true } + + +[lints] +workspace = true diff --git a/examples/simple-mcp-client-core-sse/README.md b/examples/simple-mcp-client-core-sse/README.md new file mode 100644 index 0000000..cd60280 --- /dev/null +++ b/examples/simple-mcp-client-core-sse/README.md @@ -0,0 +1,40 @@ +# Simple MCP Client Core (SSE) + +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. + +## Overview + +This project demonstrates a basic MCP client implementation, showcasing the features of rust-mcp-schema and 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. + +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. + +> Note that @modelcontextprotocol/server-everything is an npm package, so you must have Node.js and npm installed on your system, as this example attempts to start it. + +## Running the Example + +1. Clone the repository: + +```bash +git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git +cd rust-mcp-sdk +``` + +2- Start `@modelcontextprotocol/server-everything` with SSE argument: + +```bash +npx @modelcontextprotocol/server-everything sse +``` +> It launches the server, making everything accessible via the SSE transport at http://localhost:3001/sse. + +2. Open a new terminal and run the project with: + +```bash +cargo run -p simple-mcp-client-core-sse +``` + +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-core-sse/src/handler.rs b/examples/simple-mcp-client-core-sse/src/handler.rs new file mode 100644 index 0000000..3749332 --- /dev/null +++ b/examples/simple-mcp-client-core-sse/src/handler.rs @@ -0,0 +1,55 @@ +use async_trait::async_trait; +use rust_mcp_schema::{ + schema_utils::{NotificationFromServer, RequestFromServer, ResultFromClient}, + RpcError, +}; +use rust_mcp_sdk::{mcp_client::ClientHandlerCore, McpClient}; +pub struct MyClientHandler; + +// To check out a list of all the methods in the trait that you can override, take a look at +// https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_client_handler_core.rs + +#[async_trait] +impl ClientHandlerCore for MyClientHandler { + async fn handle_request( + &self, + request: RequestFromServer, + _runtime: &dyn McpClient, + ) -> std::result::Result { + match request { + RequestFromServer::ServerRequest(server_request) => match server_request { + rust_mcp_schema::ServerRequest::PingRequest(_) => { + return Ok(rust_mcp_schema::Result::default().into()); + } + rust_mcp_schema::ServerRequest::CreateMessageRequest(_create_message_request) => { + Err(RpcError::internal_error().with_message( + "CreateMessageRequest handler is not implemented".to_string(), + )) + } + rust_mcp_schema::ServerRequest::ListRootsRequest(_list_roots_request) => { + Err(RpcError::internal_error() + .with_message("ListRootsRequest handler is not implemented".to_string())) + } + }, + RequestFromServer::CustomRequest(_value) => Err(RpcError::internal_error() + .with_message("CustomRequest handler is not implemented".to_string())), + } + } + + async fn handle_notification( + &self, + _notification: NotificationFromServer, + _runtime: &dyn McpClient, + ) -> std::result::Result<(), RpcError> { + Err(RpcError::internal_error() + .with_message("handle_notification() Not implemented".to_string())) + } + + async fn handle_error( + &self, + _error: RpcError, + _runtime: &dyn McpClient, + ) -> std::result::Result<(), RpcError> { + Err(RpcError::internal_error().with_message("handle_error() Not implemented".to_string())) + } +} diff --git a/examples/simple-mcp-client-core-sse/src/inquiry_utils.rs b/examples/simple-mcp-client-core-sse/src/inquiry_utils.rs new file mode 100644 index 0000000..d6db24b --- /dev/null +++ b/examples/simple-mcp-client-core-sse/src/inquiry_utils.rs @@ -0,0 +1,222 @@ +//! This module contains utility functions for querying and displaying server capabilities. + +use colored::Colorize; +use rust_mcp_schema::CallToolRequestParams; +use rust_mcp_sdk::McpClient; +use rust_mcp_sdk::{error::SdkResult, mcp_client::ClientRuntime}; +use serde_json::json; +use std::io::Write; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; + +const GREY_COLOR: (u8, u8, u8) = (90, 90, 90); +const HEADER_SIZE: usize = 31; + +pub struct InquiryUtils { + pub client: Arc, +} + +impl InquiryUtils { + fn print_header(&self, title: &str) { + let pad = ((HEADER_SIZE as f32 / 2.0) + (title.len() as f32 / 2.0)).floor() as usize; + println!("\n{}", "=".repeat(HEADER_SIZE).custom_color(GREY_COLOR)); + println!("{:>pad$}", title.custom_color(GREY_COLOR)); + println!("{}", "=".repeat(HEADER_SIZE).custom_color(GREY_COLOR)); + } + + fn print_list(&self, list_items: Vec<(String, String)>) { + list_items.iter().enumerate().for_each(|(index, item)| { + println!("{}. {}: {}", index + 1, item.0.yellow(), item.1.cyan(),); + }); + } + + pub fn print_server_info(&self) { + self.print_header("Server info"); + let server_version = self.client.server_version().unwrap(); + println!("{} {}", "Server name:".bold(), server_version.name.cyan()); + println!( + "{} {}", + "Server version:".bold(), + server_version.version.cyan() + ); + } + + pub fn print_server_capabilities(&self) { + self.print_header("Capabilities"); + let capability_vec = [ + ("tools", self.client.server_has_tools()), + ("prompts", self.client.server_has_prompts()), + ("resources", self.client.server_has_resources()), + ("logging", self.client.server_supports_logging()), + ("experimental", self.client.server_has_experimental()), + ]; + + capability_vec.iter().for_each(|(tool_name, opt)| { + println!( + "{}: {}", + tool_name.bold(), + opt.map(|b| if b { "Yes" } else { "No" }) + .unwrap_or("Unknown") + .cyan() + ); + }); + } + + pub async fn print_tool_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support tools + if !self.client.server_has_tools().unwrap_or(false) { + return Ok(()); + } + + let tools = self.client.list_tools(None).await?; + self.print_header("Tools"); + self.print_list( + tools + .tools + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + + Ok(()) + } + + pub async fn print_prompts_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support prompts + if !self.client.server_has_prompts().unwrap_or(false) { + return Ok(()); + } + + let prompts = self.client.list_prompts(None).await?; + + self.print_header("Prompts"); + self.print_list( + prompts + .prompts + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + Ok(()) + } + + pub async fn print_resource_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support resources + if !self.client.server_has_resources().unwrap_or(false) { + return Ok(()); + } + + let resources = self.client.list_resources(None).await?; + + self.print_header("Resources"); + + self.print_list( + resources + .resources + .iter() + .map(|item| { + ( + item.name.clone(), + format!( + "( uri: {} , mime: {}", + item.uri, + item.mime_type.as_ref().unwrap_or(&"?".to_string()), + ), + ) + }) + .collect(), + ); + + Ok(()) + } + + pub async fn print_resource_templates(&self) -> SdkResult<()> { + // Return if the MCP server does not support resources + if !self.client.server_has_resources().unwrap_or(false) { + return Ok(()); + } + + let templates = self.client.list_resource_templates(None).await?; + + self.print_header("Resource Templates"); + + self.print_list( + templates + .resource_templates + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + Ok(()) + } + + pub async fn call_add_tool(&self, a: i64, b: i64) -> SdkResult<()> { + // Invoke the "add" tool with 100 and 25 as arguments, and display the result + println!( + "{}", + format!("\nCalling the \"add\" tool with {a} and {b} ...").magenta() + ); + + // Create a `Map` to represent the tool parameters + let params = json!({ + "a": a, + "b": b + }) + .as_object() + .unwrap() + .clone(); + + // invoke the tool + let result = self + .client + .call_tool(CallToolRequestParams { + name: "add".to_string(), + arguments: Some(params), + }) + .await?; + + // Retrieve the result content and print it to the stdout + let result_content = result.content.first().unwrap().as_text_content()?; + println!("{}", result_content.text.green()); + + Ok(()) + } + + pub async fn ping_n_times(&self, n: i32) { + let max_pings = n; + println!(); + for ping_index in 1..=max_pings { + print!("Ping the server ({} out of {})...", ping_index, max_pings); + std::io::stdout().flush().unwrap(); + let ping_result = self.client.ping(None).await; + print!( + "\rPing the server ({} out of {}) : {}", + ping_index, + max_pings, + if ping_result.is_ok() { + "success".bright_green() + } else { + "failed".bright_red() + } + ); + println!(); + sleep(Duration::from_secs(2)).await; + } + } +} diff --git a/examples/simple-mcp-client-core-sse/src/main.rs b/examples/simple-mcp-client-core-sse/src/main.rs new file mode 100644 index 0000000..f98432d --- /dev/null +++ b/examples/simple-mcp-client-core-sse/src/main.rs @@ -0,0 +1,91 @@ +mod handler; +mod inquiry_utils; + +use handler::MyClientHandler; + +use inquiry_utils::InquiryUtils; +use rust_mcp_schema::{ + ClientCapabilities, Implementation, InitializeRequestParams, JSONRPC_VERSION, +}; +use rust_mcp_sdk::error::SdkResult; +use rust_mcp_sdk::mcp_client::client_runtime_core; +use rust_mcp_sdk::{ClientSseTransport, ClientSseTransportOptions, McpClient}; +use std::sync::Arc; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +const MCP_SERVER_URL: &str = "http://localhost:3001/sse"; + +#[tokio::main] +async fn main() -> SdkResult<()> { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // Step1 : Define client details and capabilities + let client_details: InitializeRequestParams = InitializeRequestParams { + capabilities: ClientCapabilities::default(), + client_info: Implementation { + name: "simple-rust-mcp-client-core-sse".into(), + version: "0.1.0".into(), + }, + protocol_version: JSONRPC_VERSION.into(), + }; + + // Step2 : Create a transport, with options to launch/connect to a MCP Server + // Assuming @modelcontextprotocol/server-everything is launched with sse argument and listening on port 3001 + let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?; + + // STEP 3: instantiate our custom handler that is responsible for handling MCP messages + let handler = MyClientHandler {}; + + let client = client_runtime_core::create_client(client_details, transport, handler); + + // STEP 5: start the MCP client + client.clone().start().await?; + + // You can utilize the client and its methods to interact with the MCP Server. + // The following demonstrates how to use client methods to retrieve server information, + // and print them in the terminal, set the log level, invoke a tool, and more. + + // Create a struct with utility functions for demonstration purpose, to utilize different client methods and display the information. + let utils = InquiryUtils { + client: Arc::clone(&client), + }; + // Display server information (name and version) + utils.print_server_info(); + + // Display server capabilities + utils.print_server_capabilities(); + + // Display the list of tools available on the server + utils.print_tool_list().await?; + + // Display the list of prompts available on the server + utils.print_prompts_list().await?; + + // Display the list of resources available on the server + utils.print_resource_list().await?; + + // Display the list of resource templates available on the server + utils.print_resource_templates().await?; + + // Call add tool, and print the result + utils.call_add_tool(100, 25).await?; + + // Set the log level + utils + .client + .set_logging_level(rust_mcp_schema::LoggingLevel::Debug) + .await?; + + // Send 3 pings to the server, with a 2-second interval between each ping. + utils.ping_n_times(3).await; + client.shut_down().await?; + + Ok(()) +} diff --git a/examples/simple-mcp-client-sse/Cargo.toml b/examples/simple-mcp-client-sse/Cargo.toml new file mode 100644 index 0000000..afb8f01 --- /dev/null +++ b/examples/simple-mcp-client-sse/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "simple-mcp-client-sse" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT" + + +[dependencies] +rust-mcp-sdk = { workspace = true, default-features = false, features = [ + "client", + "macros", +] } +rust-mcp-schema = { workspace = true } + +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +colored = "3.0.0" +tracing-subscriber = { workspace = true } +tracing = { workspace = true } + + +[lints] +workspace = true diff --git a/examples/simple-mcp-client-sse/README.md b/examples/simple-mcp-client-sse/README.md new file mode 100644 index 0000000..27eeb05 --- /dev/null +++ b/examples/simple-mcp-client-sse/README.md @@ -0,0 +1,39 @@ +# Simple MCP Client (SSE) + +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. + +## Overview + +This project demonstrates a basic MCP client implementation, showcasing the features of rust-mcp-schema and 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. + +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. + +> Note that @modelcontextprotocol/server-everything is an npm package, so you must have Node.js and npm installed on your system, as this example attempts to start it. + +## Running the Example + +1. Clone the repository: + +```bash +git clone git@github.com:rust-mcp-stack/rust-mcp-sdk.git +cd rust-mcp-sdk +``` + +2- Start `@modelcontextprotocol/server-everything` with SSE argument: + +```bash +npx @modelcontextprotocol/server-everything sse +``` +> It launches the server, making everything accessible via the SSE transport at http://localhost:3001/sse. + +2. Open a new terminal and run the project with: + +```bash +cargo run -p simple-mcp-client-sse +``` + +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-sse/src/handler.rs b/examples/simple-mcp-client-sse/src/handler.rs new file mode 100644 index 0000000..93935ab --- /dev/null +++ b/examples/simple-mcp-client-sse/src/handler.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; +use rust_mcp_sdk::mcp_client::ClientHandler; + +pub struct MyClientHandler; + +#[async_trait] +impl ClientHandler for MyClientHandler { + // To check out a list of all the methods in the trait that you can override, take a look at + // https://github.com/rust-mcp-stack/rust-mcp-sdk/blob/main/crates/rust-mcp-sdk/src/mcp_handlers/mcp_client_handler.rs + // +} diff --git a/examples/simple-mcp-client-sse/src/inquiry_utils.rs b/examples/simple-mcp-client-sse/src/inquiry_utils.rs new file mode 100644 index 0000000..d6db24b --- /dev/null +++ b/examples/simple-mcp-client-sse/src/inquiry_utils.rs @@ -0,0 +1,222 @@ +//! This module contains utility functions for querying and displaying server capabilities. + +use colored::Colorize; +use rust_mcp_schema::CallToolRequestParams; +use rust_mcp_sdk::McpClient; +use rust_mcp_sdk::{error::SdkResult, mcp_client::ClientRuntime}; +use serde_json::json; +use std::io::Write; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; + +const GREY_COLOR: (u8, u8, u8) = (90, 90, 90); +const HEADER_SIZE: usize = 31; + +pub struct InquiryUtils { + pub client: Arc, +} + +impl InquiryUtils { + fn print_header(&self, title: &str) { + let pad = ((HEADER_SIZE as f32 / 2.0) + (title.len() as f32 / 2.0)).floor() as usize; + println!("\n{}", "=".repeat(HEADER_SIZE).custom_color(GREY_COLOR)); + println!("{:>pad$}", title.custom_color(GREY_COLOR)); + println!("{}", "=".repeat(HEADER_SIZE).custom_color(GREY_COLOR)); + } + + fn print_list(&self, list_items: Vec<(String, String)>) { + list_items.iter().enumerate().for_each(|(index, item)| { + println!("{}. {}: {}", index + 1, item.0.yellow(), item.1.cyan(),); + }); + } + + pub fn print_server_info(&self) { + self.print_header("Server info"); + let server_version = self.client.server_version().unwrap(); + println!("{} {}", "Server name:".bold(), server_version.name.cyan()); + println!( + "{} {}", + "Server version:".bold(), + server_version.version.cyan() + ); + } + + pub fn print_server_capabilities(&self) { + self.print_header("Capabilities"); + let capability_vec = [ + ("tools", self.client.server_has_tools()), + ("prompts", self.client.server_has_prompts()), + ("resources", self.client.server_has_resources()), + ("logging", self.client.server_supports_logging()), + ("experimental", self.client.server_has_experimental()), + ]; + + capability_vec.iter().for_each(|(tool_name, opt)| { + println!( + "{}: {}", + tool_name.bold(), + opt.map(|b| if b { "Yes" } else { "No" }) + .unwrap_or("Unknown") + .cyan() + ); + }); + } + + pub async fn print_tool_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support tools + if !self.client.server_has_tools().unwrap_or(false) { + return Ok(()); + } + + let tools = self.client.list_tools(None).await?; + self.print_header("Tools"); + self.print_list( + tools + .tools + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + + Ok(()) + } + + pub async fn print_prompts_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support prompts + if !self.client.server_has_prompts().unwrap_or(false) { + return Ok(()); + } + + let prompts = self.client.list_prompts(None).await?; + + self.print_header("Prompts"); + self.print_list( + prompts + .prompts + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + Ok(()) + } + + pub async fn print_resource_list(&self) -> SdkResult<()> { + // Return if the MCP server does not support resources + if !self.client.server_has_resources().unwrap_or(false) { + return Ok(()); + } + + let resources = self.client.list_resources(None).await?; + + self.print_header("Resources"); + + self.print_list( + resources + .resources + .iter() + .map(|item| { + ( + item.name.clone(), + format!( + "( uri: {} , mime: {}", + item.uri, + item.mime_type.as_ref().unwrap_or(&"?".to_string()), + ), + ) + }) + .collect(), + ); + + Ok(()) + } + + pub async fn print_resource_templates(&self) -> SdkResult<()> { + // Return if the MCP server does not support resources + if !self.client.server_has_resources().unwrap_or(false) { + return Ok(()); + } + + let templates = self.client.list_resource_templates(None).await?; + + self.print_header("Resource Templates"); + + self.print_list( + templates + .resource_templates + .iter() + .map(|item| { + ( + item.name.clone(), + item.description.clone().unwrap_or_default(), + ) + }) + .collect(), + ); + Ok(()) + } + + pub async fn call_add_tool(&self, a: i64, b: i64) -> SdkResult<()> { + // Invoke the "add" tool with 100 and 25 as arguments, and display the result + println!( + "{}", + format!("\nCalling the \"add\" tool with {a} and {b} ...").magenta() + ); + + // Create a `Map` to represent the tool parameters + let params = json!({ + "a": a, + "b": b + }) + .as_object() + .unwrap() + .clone(); + + // invoke the tool + let result = self + .client + .call_tool(CallToolRequestParams { + name: "add".to_string(), + arguments: Some(params), + }) + .await?; + + // Retrieve the result content and print it to the stdout + let result_content = result.content.first().unwrap().as_text_content()?; + println!("{}", result_content.text.green()); + + Ok(()) + } + + pub async fn ping_n_times(&self, n: i32) { + let max_pings = n; + println!(); + for ping_index in 1..=max_pings { + print!("Ping the server ({} out of {})...", ping_index, max_pings); + std::io::stdout().flush().unwrap(); + let ping_result = self.client.ping(None).await; + print!( + "\rPing the server ({} out of {}) : {}", + ping_index, + max_pings, + if ping_result.is_ok() { + "success".bright_green() + } else { + "failed".bright_red() + } + ); + println!(); + sleep(Duration::from_secs(2)).await; + } + } +} diff --git a/examples/simple-mcp-client-sse/src/main.rs b/examples/simple-mcp-client-sse/src/main.rs new file mode 100644 index 0000000..637de81 --- /dev/null +++ b/examples/simple-mcp-client-sse/src/main.rs @@ -0,0 +1,91 @@ +mod handler; +mod inquiry_utils; + +use handler::MyClientHandler; + +use inquiry_utils::InquiryUtils; +use rust_mcp_schema::{ + ClientCapabilities, Implementation, InitializeRequestParams, JSONRPC_VERSION, +}; +use rust_mcp_sdk::error::SdkResult; +use rust_mcp_sdk::mcp_client::client_runtime; +use rust_mcp_sdk::{ClientSseTransport, ClientSseTransportOptions, McpClient}; +use std::sync::Arc; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +const MCP_SERVER_URL: &str = "http://localhost:3001/sse"; + +#[tokio::main] +async fn main() -> SdkResult<()> { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // Step1 : Define client details and capabilities + let client_details: InitializeRequestParams = InitializeRequestParams { + capabilities: ClientCapabilities::default(), + client_info: Implementation { + name: "simple-rust-mcp-client-sse".into(), + version: "0.1.0".into(), + }, + protocol_version: JSONRPC_VERSION.into(), + }; + + // Step2 : Create a transport, with options to launch/connect to a MCP Server + // Assuming @modelcontextprotocol/server-everything is launched with sse argument and listening on port 3001 + let transport = ClientSseTransport::new(MCP_SERVER_URL, ClientSseTransportOptions::default())?; + + // STEP 3: instantiate our custom handler that is responsible for handling MCP messages + let handler = MyClientHandler {}; + + let client = client_runtime::create_client(client_details, transport, handler); + + // STEP 5: start the MCP client + client.clone().start().await?; + + // You can utilize the client and its methods to interact with the MCP Server. + // The following demonstrates how to use client methods to retrieve server information, + // and print them in the terminal, set the log level, invoke a tool, and more. + + // Create a struct with utility functions for demonstration purpose, to utilize different client methods and display the information. + let utils = InquiryUtils { + client: Arc::clone(&client), + }; + // Display server information (name and version) + utils.print_server_info(); + + // Display server capabilities + utils.print_server_capabilities(); + + // Display the list of tools available on the server + utils.print_tool_list().await?; + + // Display the list of prompts available on the server + utils.print_prompts_list().await?; + + // Display the list of resources available on the server + utils.print_resource_list().await?; + + // Display the list of resource templates available on the server + utils.print_resource_templates().await?; + + // Call add tool, and print the result + utils.call_add_tool(100, 25).await?; + + // Set the log level + utils + .client + .set_logging_level(rust_mcp_schema::LoggingLevel::Debug) + .await?; + + // Send 3 pings to the server, with a 2-second interval between each ping. + utils.ping_n_times(3).await; + client.shut_down().await?; + + Ok(()) +}