feat(mcp): 实现HTTP统一方案并添加MCP文档

- 将MCP服务和Mock API合并到单个HTTP服务器(8080端口)
- 添加POST /mcp端点,使用无状态StreamableHttpService
- 新增docs/mcp-implementation.md文档
This commit is contained in:
2026-03-29 22:30:29 +08:00
parent d364307131
commit cfcebbe300
7 changed files with 1125 additions and 124 deletions

581
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -11,6 +20,23 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -77,9 +103,9 @@ dependencies = [
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@@ -141,12 +167,77 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.20" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@@ -169,6 +260,18 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -284,10 +387,44 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 5.3.0",
"wasip2", "wasip2",
] ]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@@ -394,6 +531,30 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@@ -456,6 +617,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.178" version = "0.2.178"
@@ -483,6 +650,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.8.4" version = "0.8.4"
@@ -529,6 +705,7 @@ dependencies = [
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"walkdir", "walkdir",
] ]
@@ -578,6 +755,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -617,10 +800,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "paste" name = "pastey"
version = "1.0.15" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@@ -640,6 +823,31 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[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.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.103"
@@ -664,6 +872,41 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -674,34 +917,83 @@ dependencies = [
] ]
[[package]] [[package]]
name = "rmcp" name = "ref-cast"
version = "0.1.5" version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33a0110d28bd076f39e14bfd5b0340216dd18effeb5d02b43215944cc3e5c751" checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [ dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rmcp"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5df440eaa43f8573491ed4a5899719b6d29099500774abba12214a095a4083ed"
dependencies = [
"async-trait",
"base64", "base64",
"bytes",
"chrono", "chrono",
"futures", "futures",
"paste", "http",
"http-body",
"http-body-util",
"pastey",
"pin-project-lite", "pin-project-lite",
"rand",
"rmcp-macros", "rmcp-macros",
"schemars", "schemars",
"serde", "serde",
"serde_json", "serde_json",
"sse-stream",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream",
"tokio-util", "tokio-util",
"tower-service",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]
name = "rmcp-macros" name = "rmcp-macros"
version = "0.1.5" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6e2b2fd7497540489fa2db285edd43b7ed14c49157157438664278da6e42a7a" checksum = "9ef03779cccab8337dd8617c53fce5c98ec21794febc397531555472ca28f8c3"
dependencies = [ dependencies = [
"darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_json",
"syn", "syn",
] ]
@@ -741,11 +1033,13 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.22" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [ dependencies = [
"chrono",
"dyn-clone", "dyn-clone",
"ref-cast",
"schemars_derive", "schemars_derive",
"serde", "serde",
"serde_json", "serde_json",
@@ -753,9 +1047,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "0.8.22" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -769,6 +1063,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -892,6 +1192,25 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "sse-stream"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
dependencies = [
"bytes",
"futures-util",
"http-body",
"http-body-util",
"pin-project-lite",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.111" version = "2.0.111"
@@ -916,7 +1235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -951,6 +1270,37 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.48.0" version = "1.48.0"
@@ -979,6 +1329,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.17" version = "0.7.17"
@@ -1032,6 +1393,18 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.31"
@@ -1070,10 +1443,14 @@ version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [ dependencies = [
"matchers",
"nu-ansi-term", "nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"thread_local", "thread_local",
"tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
] ]
@@ -1084,6 +1461,23 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@@ -1112,7 +1506,16 @@ version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [ dependencies = [
"wit-bindgen", "wit-bindgen 0.46.0",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
] ]
[[package]] [[package]]
@@ -1160,6 +1563,40 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.10.0",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -1317,6 +1754,114 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "0.1.8" version = "0.1.8"

View File

@@ -1,11 +1,7 @@
[package] [package]
name = "mock_server" name = "mock_server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[features]
default = []
mcp = ["rmcp", "schemars"]
[dependencies] [dependencies]
# 核心 Web 框架 # 核心 Web 框架
@@ -23,16 +19,18 @@ serde_json = "1.0.147"
# 物理目录递归扫描工具 # 物理目录递归扫描工具
walkdir = "2.5.0" walkdir = "2.5.0"
tracing="0.1.44" # 日志系统
tracing-subscriber = "0.3.22" tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["fmt", "env-filter"] }
tracing-appender = "0.2"
# 热加载支持(扩展功能) # 热加载支持(扩展功能)
notify = "8.2.0" notify = "8.2.0"
notify-debouncer-mini = "0.6.0" notify-debouncer-mini = "0.6.0"
# MCP Server 支持(可选) # MCP Server 支持
rmcp = { version = "0.1", features = ["server"], optional = true } rmcp = { version = "0.11", features = ["server", "transport-streamable-http-server", "transport-streamable-http-server-session"] }
schemars = { version = "0.8", optional = true } schemars = "1.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3.24.0" tempfile = "3.24.0"

427
docs/mcp-implementation.md Normal file
View File

@@ -0,0 +1,427 @@
# rmcp 0.11 MCP Server 实现指南
本文档详细介绍了如何使用 rmcp 0.11 实现 MCP Server包括核心概念、关键代码模式和常见陷阱。
## 目录
- [核心概念](#核心概念)
- [Tool 注册三要素](#tool-注册三要素)
- [完整代码示例](#完整代码示例)
- [常见错误和解决方案](#常见错误和解决方案)
- [HTTP 传输配置](#http-传输配置)
- [客户端配置](#客户端配置)
---
## 核心概念
### rmcp 简介
rmcp 是 Rust 官方的 MCP SDK提供了实现 MCP (Model Context Protocol) 服务器和客户端的完整工具链。
### MCP 协议
MCP (Model Context Protocol) 是一个开放协议,用于连接 AI 助手与外部系统。它定义了一套标准化的接口,允许 AI 模型:
- **Tools**: 调用外部工具/函数
- **Resources**: 访问外部资源
- **Prompts**: 使用预定义的提示模板
### 依赖配置
```toml
# Cargo.toml
[dependencies]
rmcp = { version = "0.11", features = ["server", "transport-streamable-http-server", "transport-streamable-http-server-session"] }
schemars = "1.0"
tokio-util = { version = "0.7", features = ["io"] }
```
---
## Tool 注册三要素
要使 MCP tools 在 rmcp 0.11 中正常工作,**必须**同时具备以下三个要素:
### 1. `#[tool_router]` 宏
放在包含 tool 方法的 impl 块上:
```rust
#[tool_router]
impl MockMcpServer {
// tool 方法...
}
```
### 2. `tool_router: ToolRouter<Self>` 字段
在结构体中必须包含此字段:
```rust
#[derive(Clone)]
pub struct MockMcpServer {
manager: Arc<MockManager>,
tool_router: ToolRouter<Self>, // 必需字段
}
```
### 3. `#[tool_handler]` 宏
放在 `ServerHandler` trait 实现块上:
```rust
#[tool_handler]
impl ServerHandler for MockMcpServer {
fn get_info(&self) -> ServerInfo {
// ...
}
}
```
> **重要**: 缺少 `#[tool_handler]` 会导致 `tools/list` 返回空数组 `{"tools": []}`
---
## 完整代码示例
### 结构体定义
```rust
use std::sync::Arc;
use rmcp::{
handler::server::tool::ToolRouter,
handler::server::wrapper::Parameters,
model::*,
tool, tool_handler, tool_router,
ErrorData as McpError, ServerHandler,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// MCP Server for Mock Server management
#[derive(Clone)]
pub struct MockMcpServer {
manager: Arc<MockManager>,
tool_router: ToolRouter<Self>, // 必需字段
}
```
### 请求参数结构体
```rust
/// 使用 schemars 和 serde 自动生成 JSON Schema
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetRuleRequest {
#[schemars(description = "Group name (directory name)")]
pub group: String,
#[schemars(description = "Rule name")]
pub name: String,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListRulesRequest {
#[schemars(description = "Optional group name to filter by")]
pub group: Option<String>,
}
```
### Tool 方法实现
```rust
#[tool_router]
impl MockMcpServer {
pub fn new(manager: Arc<MockManager>) -> Self {
Self {
manager,
tool_router: Self::tool_router(), // 初始化 tool_router
}
}
/// 无参数 tool
#[tool(description = "List all groups (directories)")]
async fn list_groups(&self) -> Result<CallToolResult, McpError> {
let groups = self.manager.list_groups();
let result = serde_json::to_string_pretty(&groups)
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::text(result)]))
}
/// 带参数 tool - 使用 Parameters<T> 包装器
#[tool(description = "Get a specific mock rule by group and name")]
async fn get_mock_rule(
&self,
params: Parameters<GetRuleRequest>,
) -> Result<CallToolResult, McpError> {
match self.manager.get(&params.0.group, &params.0.name) {
Some(rule) => {
let result = serde_json::to_string_pretty(&rule)
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::text(result)]))
}
None => Ok(CallToolResult::error(vec![Content::text(
format!("Rule not found: {}/{}", params.0.group, params.0.name),
)])),
}
}
/// 可选参数
#[tool(description = "List all mock rules, optionally filtered by group")]
async fn list_mock_rules(
&self,
params: Parameters<ListRulesRequest>,
) -> Result<CallToolResult, McpError> {
let rules = self.manager.list(params.0.group.as_deref());
let result = serde_json::to_string_pretty(&rules)
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::text(result)]))
}
}
```
### ServerHandler 实现
```rust
#[tool_handler] // 关键宏!
impl ServerHandler for MockMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder()
.enable_tools() // 必须启用 tools 能力
.build(),
instructions: Some("Mock Server MCP - Manage mock API rules.".to_string()),
..Default::default()
}
}
}
```
---
## 常见错误和解决方案
### 1. Missing `#[tool_handler]`
**症状**: `tools/list` 返回空数组 `{"tools": []}`
**原因**: 没有在 `ServerHandler` impl 块上添加 `#[tool_handler]`
**解决方案**:
```rust
// 错误 - 缺少宏
impl ServerHandler for MockMcpServer {
fn get_info(&self) -> ServerInfo { ... }
}
// 正确
#[tool_handler]
impl ServerHandler for MockMcpServer {
fn get_info(&self) -> ServerInfo { ... }
}
```
### 2. Missing `enable_tools()`
**症状**: Tools 不被广播,客户端无法发现 tools
**原因**: `ServerCapabilities` 没有启用 tools
**解决方案**:
```rust
// 错误
ServerInfo {
capabilities: ServerCapabilities::default (),
...
}
// 正确
ServerInfo {
capabilities: ServerCapabilities::builder()
.enable_tools()
.build(),
...
}
```
### 3. 使用 `#[tool(aggr)]`
**症状**: 编译错误
**原因**: rmcp 0.11 不支持 `aggr` 参数
**解决方案**:
```rust
// 错误 - rmcp 0.11 不支持
#[tool(aggr)]
async fn my_tool(&self, params: MyRequest) -> Result<...>
// 正确 - 使用 Parameters<T> 包装器
#[tool(description = "...")]
async fn my_tool(&self, params: Parameters<MyRequest>) -> Result<...>
```
### 4. 使用 `#[tool(tool_box)]`
**症状**: 编译错误
**原因**: rmcp 0.11 使用不同的宏名称
**解决方案**:
```rust
// 错误
#[tool(tool_box)]
impl MockMcpServer { ... }
// 正确
#[tool_router]
impl MockMcpServer { ... }
```
---
## HTTP 传输配置
### StreamableHttpService 无状态模式
适用于简单的 HTTP 集成,无需维护会话状态:
```rust
use rmcp::transport::streamable_http_server::{
StreamableHttpService, StreamableHttpServerConfig,
session::never::NeverSessionManager,
};
use tokio_util::sync::CancellationToken;
/// 创建无状态 MCP HTTP 服务
pub fn create_mcp_http_service(
manager: Arc<MockManager>,
) -> StreamableHttpService<MockMcpServer, NeverSessionManager> {
StreamableHttpService::new(
// 每次请求创建新的 server 实例
move || Ok(MockMcpServer::new(manager.clone())),
// 无状态会话管理器
Arc::new(NeverSessionManager::default()),
StreamableHttpServerConfig {
sse_keep_alive: None, // SSE 保活配置(可选)
stateful_mode: false, // 无状态模式
cancellation_token: CancellationToken::new(),
},
)
}
```
### 与 Axum 集成
```rust
use axum::{
routing::post,
Router,
body::Body,
http::Request,
};
let mcp_service = create_mcp_http_service(manager);
let app = Router::new()
.route("/mcp", post({
let service = mcp_service.clone();
move | req: Request < Body > | {
let service = service.clone();
async move { service.handle(req).await }
}
}));
```
### 配置选项说明
| 选项 | 类型 | 说明 |
|----------------------|---------------------|------------|
| `sse_keep_alive` | `Option<Duration>` | SSE 连接保活间隔 |
| `stateful_mode` | `bool` | 是否维护会话状态 |
| `cancellation_token` | `CancellationToken` | 用于优雅关闭 |
---
## 客户端配置
### Claude Code 配置
在 Claude Code 的 `settings.json` 中添加:
```json
{
"mcpServers": {
"mock-server": {
"type": "http",
"url": "http://127.0.0.1:8080/mcp"
}
}
}
```
### Claude Desktop 配置
在 Claude Desktop 的配置文件中添加:
```json
{
"mcpServers": {
"mock-server": {
"url": "http://127.0.0.1:8080/mcp"
}
}
}
```
---
## 架构说明
```
┌─────────────────────────────────────────────────────────────┐
│ HTTP Server (Axum) │
│ Port 8080 │
├─────────────────────────────────────────────────────────────┤
│ │
│ POST /mcp │ /* (fallback) │
│ ├─ tools/list │ Mock API endpoints │
│ ├─ tools/call │ │
│ └─ other MCP methods │ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ StreamableHttpService<MockMcpServer> │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ MockMcpServer │ │ NeverSessionManager │ │
│ │ │ │ (无状态) │ │
│ │ - tool_router │ │ │ │
│ │ - manager │ │ │ │
│ └─────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MockManager (Arc) │
│ │
│ 共享于 HTTP MCP 端点和 Mock API handler │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 参考资源
- [rmcp GitHub Repository](https://github.com/anthropics/rmcp)
- [MCP Specification](https://spec.modelcontextprotocol.io/)
- [schemars Documentation](https://docs.rs/schemars/)

View File

@@ -4,6 +4,5 @@ pub mod loader;
pub mod router; pub mod router;
pub mod handler; pub mod handler;
pub mod manager; pub mod manager;
pub mod logging;
#[cfg(feature = "mcp")]
pub mod mcp; pub mod mcp;

View File

@@ -3,37 +3,39 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use axum::{routing::any, Router}; use axum::{routing::post, Router};
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
use tracing::{info, error};
use mock_server::loader::MockLoader; use mock_server::loader::MockLoader;
use mock_server::router::MockRouter; use mock_server::router::MockRouter;
use mock_server::handler::{mock_handler, AppState}; use mock_server::handler::{mock_handler, AppState};
use mock_server::logging;
fn print_usage() { fn print_usage() {
println!("Mock Server - A mock API server with hot-reload support"); println!("Mock Server - A mock API server with hot-reload and MCP support");
println!(); println!();
println!("Usage: mock_server [OPTIONS]"); println!("Usage: mock_server [OPTIONS]");
println!(); println!();
println!("Options:"); println!("Options:");
println!(" --mcp Run as MCP server (stdio transport)");
println!(" --mocks <DIR> Mocks directory path (default: ./mocks)"); println!(" --mocks <DIR> Mocks directory path (default: ./mocks)");
println!(" --port <PORT> Server port (default: 8080)"); println!(" --port <PORT> HTTP server port (default: 8080)");
println!(" --help Show this help message"); println!(" --help Show this help message");
println!();
println!("MCP Endpoint: POST http://127.0.0.1:<PORT>/mcp");
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init(); // Initialize logging system
let log_dir = PathBuf::from("./logs");
logging::init(log_dir);
// 解析命令行参数 // Parse command line arguments
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let mut mocks_dir = PathBuf::from("./mocks"); let mut mocks_dir = PathBuf::from("./mocks");
let mut port: u16 = 8080; let mut port: u16 = 8080;
#[cfg(feature = "mcp")]
let mut mcp_mode = false;
let mut i = 1; let mut i = 1;
while i < args.len() { while i < args.len() {
match args[i].as_str() { match args[i].as_str() {
@@ -41,17 +43,6 @@ async fn main() {
print_usage(); print_usage();
return; return;
} }
"--mcp" => {
#[cfg(feature = "mcp")]
{
mcp_mode = true;
}
#[cfg(not(feature = "mcp"))]
{
eprintln!("MCP feature not enabled. Rebuild with --features mcp");
std::process::exit(1);
}
}
"--mocks" => { "--mocks" => {
if i + 1 < args.len() { if i + 1 < args.len() {
mocks_dir = PathBuf::from(&args[i + 1]); mocks_dir = PathBuf::from(&args[i + 1]);
@@ -89,37 +80,25 @@ async fn main() {
std::fs::create_dir_all(&mocks_dir).unwrap(); std::fs::create_dir_all(&mocks_dir).unwrap();
} }
#[cfg(feature = "mcp")] // Create shared MockManager for both HTTP and MCP
if mcp_mode { let manager = Arc::new(mock_server::manager::MockManager::new(mocks_dir.clone()));
run_mcp_server(mocks_dir).await; info!("Loaded {} groups", manager.list_groups().len());
return;
}
run_http_server(mocks_dir, port).await; // Run unified HTTP server (includes MCP endpoint)
run_http_server(mocks_dir, port, manager).await;
} }
#[cfg(feature = "mcp")] async fn run_http_server(mocks_dir: PathBuf, port: u16, manager: Arc<mock_server::manager::MockManager>) {
async fn run_mcp_server(mocks_dir: PathBuf) { info!("Scanning mocks directory...");
println!("Starting MCP server...");
let manager = Arc::new(mock_server::manager::MockManager::new(mocks_dir));
println!("Loaded {} groups", manager.list_groups().len());
match mock_server::mcp::run_mcp_server(manager).await {
Ok(_) => println!("MCP server stopped"),
Err(e) => eprintln!("MCP server error: {}", e),
}
}
async fn run_http_server(mocks_dir: PathBuf, port: u16) {
println!("Scanning mocks directory...");
let index = MockLoader::load_all_from_dir(&mocks_dir); let index = MockLoader::load_all_from_dir(&mocks_dir);
let shared_state = Arc::new(AppState { let shared_state = Arc::new(AppState {
router: std::sync::RwLock::new(MockRouter::new(index)), router: std::sync::RwLock::new(MockRouter::new(index)),
}); });
// 设置热加载监听器 // Setup hot-reload watcher
let state_for_watcher = shared_state.clone(); let state_for_watcher = shared_state.clone();
let watch_path = mocks_dir.clone(); let watch_path = mocks_dir.clone();
let manager_for_watcher = manager.clone();
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap(); let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap();
@@ -129,24 +108,37 @@ async fn run_http_server(mocks_dir: PathBuf, port: u16) {
while let Ok(res) = rx.recv() { while let Ok(res) = rx.recv() {
match res { match res {
Ok(_) => { Ok(_) => {
println!("🔄 Detecting changes in mocks/, reloading..."); info!("Detected changes in mocks/, reloading...");
let new_index = MockLoader::load_all_from_dir(&watch_path); let new_index = MockLoader::load_all_from_dir(&watch_path);
let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock"); let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock");
*writer = MockRouter::new(new_index); *writer = MockRouter::new(new_index);
println!("✅ Mocks reloaded successfully."); // Also reload in manager
manager_for_watcher.reload();
info!("Mocks reloaded successfully.");
} }
Err(e) => eprintln!("Watcher error: {:?}", e), Err(e) => error!("Watcher error: {:?}", e),
} }
} }
}); });
// Create MCP HTTP service (stateless)
let mcp_service = mock_server::mcp::create_mcp_http_service(manager.clone());
let app = Router::new() let app = Router::new()
.fallback(any(mock_handler)) // MCP endpoint (stateless HTTP transport)
.route("/mcp", post(|req| async move {
mcp_service.handle(req).await
}))
// Mock API fallback
.fallback(axum::routing::any(mock_handler))
.with_state(shared_state); .with_state(shared_state);
let addr = SocketAddr::from(([127, 0, 0, 1], port)); let addr = SocketAddr::from(([127, 0, 0, 1], port));
println!("🚀 Server running at http://{}", addr); info!("HTTP server running at http://{}", addr);
info!("MCP endpoint available at http://{}/mcp", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap(); if let Err(e) = axum::serve(listener, app).await {
error!("HTTP server error: {}", e);
}
} }

View File

@@ -1,3 +1,3 @@
mod server; mod server;
pub use server::run_mcp_server; pub use server::create_mcp_http_service;

View File

@@ -1,13 +1,14 @@
use std::sync::Arc;
use rmcp::{ use rmcp::{
handler::server::tool::ToolRouter, handler::server::tool::ToolRouter,
handler::server::wrapper::Parameters,
model::*, model::*,
tool, tool_handler, tool_router, tool, tool_handler, tool_router,
ServerHandler, ServiceExt, ErrorData as McpError, ServerHandler,
transport::stdio,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::manager::MockManager; use crate::manager::MockManager;
use crate::models::MockRule; use crate::models::MockRule;
@@ -19,6 +20,51 @@ pub struct MockMcpServer {
tool_router: ToolRouter<Self>, tool_router: ToolRouter<Self>,
} }
/// Request for getting a specific mock rule
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetRuleRequest {
#[schemars(description = "Group name (directory name)")]
pub group: String,
#[schemars(description = "Rule name")]
pub name: String,
}
/// Request for creating a new mock rule
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateRuleRequest {
#[schemars(description = "Group name (directory name)")]
pub group: String,
#[schemars(description = "Mock rule definition (JSON object)")]
pub rule_json: String,
}
/// Request for updating a mock rule
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateRuleRequest {
#[schemars(description = "Group name")]
pub group: String,
#[schemars(description = "Current rule name")]
pub name: String,
#[schemars(description = "Updated rule definition (JSON object)")]
pub rule_json: String,
}
/// Request for deleting a mock rule
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteRuleRequest {
#[schemars(description = "Group name")]
pub group: String,
#[schemars(description = "Rule name")]
pub name: String,
}
/// Request for listing mock rules with optional filter
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListRulesRequest {
#[schemars(description = "Optional group name to filter by")]
pub group: Option<String>,
}
#[tool_router] #[tool_router]
impl MockMcpServer { impl MockMcpServer {
pub fn new(manager: Arc<MockManager>) -> Self { pub fn new(manager: Arc<MockManager>) -> Self {
@@ -31,10 +77,9 @@ impl MockMcpServer {
#[tool(description = "List all mock rules, optionally filtered by group")] #[tool(description = "List all mock rules, optionally filtered by group")]
async fn list_mock_rules( async fn list_mock_rules(
&self, &self,
#[schemars(description = "Optional group name to filter by")] params: Parameters<ListRulesRequest>,
group: Option<String>,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
let rules = self.manager.list(group.as_deref()); let rules = self.manager.list(params.0.group.as_deref());
let result = serde_json::to_string_pretty(&rules) let result = serde_json::to_string_pretty(&rules)
.map_err(|e| McpError::internal_error(e.to_string(), None))?; .map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::text(result)])) Ok(CallToolResult::success(vec![Content::text(result)]))
@@ -43,19 +88,16 @@ impl MockMcpServer {
#[tool(description = "Get a specific mock rule by group and name")] #[tool(description = "Get a specific mock rule by group and name")]
async fn get_mock_rule( async fn get_mock_rule(
&self, &self,
#[schemars(description = "Group name (directory name)")] params: Parameters<GetRuleRequest>,
group: String,
#[schemars(description = "Rule name")]
name: String,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
match self.manager.get(&group, &name) { match self.manager.get(&params.0.group, &params.0.name) {
Some(rule) => { Some(rule) => {
let result = serde_json::to_string_pretty(&rule) let result = serde_json::to_string_pretty(&rule)
.map_err(|e| McpError::internal_error(e.to_string(), None))?; .map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::text(result)])) Ok(CallToolResult::success(vec![Content::text(result)]))
} }
None => Ok(CallToolResult::error(vec![Content::text( None => Ok(CallToolResult::error(vec![Content::text(
format!("Rule not found: {}/{}", group, name), format!("Rule not found: {}/{}", params.0.group, params.0.name),
)])), )])),
} }
} }
@@ -63,17 +105,14 @@ impl MockMcpServer {
#[tool(description = "Create a new mock rule")] #[tool(description = "Create a new mock rule")]
async fn create_mock_rule( async fn create_mock_rule(
&self, &self,
#[schemars(description = "Group name (directory name)")] params: Parameters<CreateRuleRequest>,
group: String,
#[schemars(description = "Mock rule definition (JSON object)")]
rule_json: String,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
let rule: MockRule = serde_json::from_str(&rule_json) let rule: MockRule = serde_json::from_str(&params.0.rule_json)
.map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?; .map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?;
match self.manager.create(&group, rule) { match self.manager.create(&params.0.group, rule) {
Ok(_) => Ok(CallToolResult::success(vec![Content::text( Ok(_) => Ok(CallToolResult::success(vec![Content::text(
format!("Created rule in group '{}'", group), format!("Created rule in group '{}'", params.0.group),
)])), )])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])), Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
} }
@@ -82,19 +121,14 @@ impl MockMcpServer {
#[tool(description = "Update an existing mock rule")] #[tool(description = "Update an existing mock rule")]
async fn update_mock_rule( async fn update_mock_rule(
&self, &self,
#[schemars(description = "Group name")] params: Parameters<UpdateRuleRequest>,
group: String,
#[schemars(description = "Current rule name")]
name: String,
#[schemars(description = "Updated rule definition (JSON object)")]
rule_json: String,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
let rule: MockRule = serde_json::from_str(&rule_json) let rule: MockRule = serde_json::from_str(&params.0.rule_json)
.map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?; .map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?;
match self.manager.update(&group, &name, rule) { match self.manager.update(&params.0.group, &params.0.name, rule) {
Ok(_) => Ok(CallToolResult::success(vec![Content::text( Ok(_) => Ok(CallToolResult::success(vec![Content::text(
format!("Updated rule {}/{}", group, name), format!("Updated rule {}/{}", params.0.group, params.0.name),
)])), )])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])), Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
} }
@@ -103,14 +137,11 @@ impl MockMcpServer {
#[tool(description = "Delete a mock rule")] #[tool(description = "Delete a mock rule")]
async fn delete_mock_rule( async fn delete_mock_rule(
&self, &self,
#[schemars(description = "Group name")] params: Parameters<DeleteRuleRequest>,
group: String,
#[schemars(description = "Rule name")]
name: String,
) -> Result<CallToolResult, McpError> { ) -> Result<CallToolResult, McpError> {
match self.manager.delete(&group, &name) { match self.manager.delete(&params.0.group, &params.0.name) {
Ok(_) => Ok(CallToolResult::success(vec![Content::text( Ok(_) => Ok(CallToolResult::success(vec![Content::text(
format!("Deleted rule {}/{}", group, name), format!("Deleted rule {}/{}", params.0.group, params.0.name),
)])), )])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])), Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
} }
@@ -120,7 +151,7 @@ impl MockMcpServer {
async fn reload_mock_rules(&self) -> Result<CallToolResult, McpError> { async fn reload_mock_rules(&self) -> Result<CallToolResult, McpError> {
self.manager.reload(); self.manager.reload();
Ok(CallToolResult::success(vec![Content::text( Ok(CallToolResult::success(vec![Content::text(
"Reloaded all rules from disk", "Reloaded all rules from disk".to_string(),
)])) )]))
} }
@@ -137,23 +168,32 @@ impl MockMcpServer {
impl ServerHandler for MockMcpServer { impl ServerHandler for MockMcpServer {
fn get_info(&self) -> ServerInfo { fn get_info(&self) -> ServerInfo {
ServerInfo { ServerInfo {
protocol_version: ProtocolVersion::V_2024_11_05,
capabilities: ServerCapabilities::builder() capabilities: ServerCapabilities::builder()
.enable_tools() .enable_tools()
.build(), .build(),
server_info: Implementation {
name: "mock-server".to_string(),
version: "0.1.0".to_string(),
},
instructions: Some("Mock Server MCP - Manage mock API rules for development.\n\nUse list_mock_rules to see all rules, create_mock_rule to add new ones, and delete_mock_rule to remove them.".to_string()), instructions: Some("Mock Server MCP - Manage mock API rules for development.\n\nUse list_mock_rules to see all rules, create_mock_rule to add new ones, and delete_mock_rule to remove them.".to_string()),
..Default::default()
} }
} }
} }
/// Run MCP server with stdio transport use rmcp::transport::streamable_http_server::{
pub async fn run_mcp_server(manager: Arc<MockManager>) -> Result<(), Box<dyn std::error::Error>> { StreamableHttpService, StreamableHttpServerConfig,
let server = MockMcpServer::new(manager); session::never::NeverSessionManager,
let service = server.serve(stdio()).await?; };
service.waiting().await?; use tokio_util::sync::CancellationToken;
Ok(())
/// Create stateless MCP HTTP service for integration with Axum
pub fn create_mcp_http_service(
manager: Arc<MockManager>,
) -> StreamableHttpService<MockMcpServer, NeverSessionManager> {
StreamableHttpService::new(
move || Ok(MockMcpServer::new(manager.clone())),
Arc::new(NeverSessionManager::default()),
StreamableHttpServerConfig {
sse_keep_alive: None,
stateful_mode: false,
cancellation_token: CancellationToken::new(),
},
)
} }