feat: implement rest vertical slice
This commit is contained in:
Generated
+385
-6
@@ -122,6 +122,12 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -150,6 +156,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -371,8 +383,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -504,6 +532,24 @@ dependencies = [
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -512,13 +558,21 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -633,12 +687,38 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -684,6 +764,12 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -704,7 +790,14 @@ name = "mcp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mcpaas-core",
|
||||
"mcpaas-mapping",
|
||||
"mcpaas-registry",
|
||||
"mcpaas-runtime",
|
||||
"mcpaas-schema",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -735,10 +828,13 @@ dependencies = [
|
||||
name = "mcpaas-adapter-rest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mcpaas-core",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -790,6 +886,7 @@ dependencies = [
|
||||
name = "mcpaas-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mcpaas-adapter-graphql",
|
||||
"mcpaas-adapter-grpc",
|
||||
"mcpaas-adapter-rest",
|
||||
@@ -799,6 +896,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -949,6 +1047,61 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -958,6 +1111,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -965,8 +1124,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -976,7 +1145,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[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 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -985,7 +1164,16 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
@@ -1023,6 +1211,44 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -1031,12 +1257,18 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
@@ -1057,6 +1289,7 @@ version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1071,6 +1304,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
@@ -1321,7 +1560,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1372,6 +1611,9 @@ 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"
|
||||
@@ -1464,6 +1706,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -1491,6 +1743,24 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -1565,6 +1835,12 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
@@ -1640,18 +1916,115 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -1843,6 +2216,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
|
||||
@@ -22,6 +22,7 @@ version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/registry-storage`
|
||||
### `feat/rest-vertical-slice`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
|
||||
- migrations create tables from `database-schema`
|
||||
- version snapshots persist and load correctly
|
||||
- publish linkage works
|
||||
- auth profiles and artifact metadata are stored
|
||||
- registry integration tests run on a real SQLite database
|
||||
- REST operation can be created, tested, and published
|
||||
- runtime executes REST operation end-to-end
|
||||
- published REST tool is callable through MCP layer
|
||||
- negative tests for mapping and external errors exist
|
||||
- at least one demo REST scenario is automated
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/rest-vertical-slice`
|
||||
- `feat/admin-api-v1`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -7,7 +7,16 @@ version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum.workspace = true
|
||||
mcpaas-registry = { path = "../../crates/mcpaas-registry" }
|
||||
mcpaas-runtime = { path = "../../crates/mcpaas-runtime" }
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
mcpaas-core = { path = "../../crates/mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../../crates/mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../../crates/mcpaas-schema" }
|
||||
reqwest.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
||||
+332
-4
@@ -1,10 +1,23 @@
|
||||
use std::{env, net::SocketAddr};
|
||||
use std::{env, net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{Json, Router, routing::get};
|
||||
use serde_json::json;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
routing::{get, post},
|
||||
};
|
||||
use mcpaas_registry::PostgresRegistry;
|
||||
use mcpaas_runtime::{RuntimeExecutor, RuntimeOperation};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
registry: PostgresRegistry,
|
||||
runtime: RuntimeExecutor,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
@@ -14,9 +27,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.init();
|
||||
|
||||
let database_url = env::var("MCPAAS_DATABASE_URL")?;
|
||||
let bind_addr = env::var("MCPAAS_MCP_BIND").unwrap_or_else(|_| "0.0.0.0:3002".into());
|
||||
let socket_addr: SocketAddr = bind_addr.parse()?;
|
||||
let app = Router::new().route("/health", get(health));
|
||||
let registry = PostgresRegistry::connect(&database_url).await?;
|
||||
let state = Arc::new(AppState {
|
||||
registry,
|
||||
runtime: RuntimeExecutor::new(),
|
||||
});
|
||||
let app = build_app(state);
|
||||
let listener = TcpListener::bind(socket_addr).await?;
|
||||
|
||||
info!("mcp-server listening on {}", socket_addr);
|
||||
@@ -26,9 +45,318 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_app(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/tools", get(list_tools))
|
||||
.route("/tools/{tool_name}", post(call_tool))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn health() -> Json<serde_json::Value> {
|
||||
Json(json!({
|
||||
"service": "mcp-server",
|
||||
"status": "ok"
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
|
||||
let operations = state
|
||||
.registry
|
||||
.list_published_operations()
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let tools = operations
|
||||
.into_iter()
|
||||
.map(|operation| {
|
||||
json!({
|
||||
"name": operation.name,
|
||||
"title": operation.tool_description.title,
|
||||
"description": operation.tool_description.description
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(json!({ "tools": tools })))
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
Path(tool_name): Path<String>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(input): Json<Value>,
|
||||
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
|
||||
let operation = state
|
||||
.registry
|
||||
.list_published_operations()
|
||||
.await
|
||||
.map_err(internal_error)?
|
||||
.into_iter()
|
||||
.find(|operation| operation.name == tool_name)
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"error": {
|
||||
"code": "not_found",
|
||||
"message": format!("tool {tool_name} was not found")
|
||||
}
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let output = state
|
||||
.runtime
|
||||
.execute(&RuntimeOperation::from(operation), &input)
|
||||
.await
|
||||
.map_err(runtime_error)?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"tool_name": tool_name,
|
||||
"result": output
|
||||
})))
|
||||
}
|
||||
|
||||
fn internal_error(error: impl std::fmt::Display) -> (StatusCode, Json<Value>) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": {
|
||||
"code": "internal_error",
|
||||
"message": error.to_string()
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn runtime_error(error: impl std::fmt::Display) -> (StatusCode, Json<Value>) {
|
||||
(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(json!({
|
||||
"error": {
|
||||
"code": "runtime_error",
|
||||
"message": error.to_string()
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env,
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use mcpaas_core::{
|
||||
ExecutionConfig, HttpMethod, Operation, OperationId, OperationStatus, Protocol, RestTarget,
|
||||
Target, ToolDescription,
|
||||
};
|
||||
use mcpaas_mapping::{MappingRule, MappingSet};
|
||||
use mcpaas_registry::{PostgresRegistry, PublishRequest};
|
||||
use mcpaas_runtime::RuntimeExecutor;
|
||||
use mcpaas_schema::{Schema, SchemaKind};
|
||||
use serde_json::{Value, json};
|
||||
use sqlx::{Executor, postgres::PgPoolOptions};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{AppState, build_app};
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_and_calls_published_rest_tool() {
|
||||
let registry = test_registry().await;
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let operation = test_operation(&upstream_base_url);
|
||||
|
||||
registry
|
||||
.create_operation(&operation, Some("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.publish_operation(PublishRequest {
|
||||
operation_id: &operation.id,
|
||||
version: 1,
|
||||
published_at: "2026-03-26T10:00:00Z",
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
registry,
|
||||
runtime: RuntimeExecutor::new(),
|
||||
});
|
||||
let base_url = spawn_mcp_server(build_app(state)).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let tools = client
|
||||
.get(format!("{base_url}/tools"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let call_result = client
|
||||
.post(format!("{base_url}/tools/crm_create_lead"))
|
||||
.json(&json!({ "email": "user@example.com" }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tools["tools"][0]["name"], "crm_create_lead");
|
||||
assert_eq!(call_result["result"], json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
async fn spawn_upstream_server() -> String {
|
||||
let app = Router::new().route("/crm/leads", post(create_lead));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn spawn_mcp_server(app: Router) -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_lead(Json(payload): Json<Value>) -> Json<Value> {
|
||||
Json(json!({
|
||||
"id": "lead_123",
|
||||
"email": payload["email"]
|
||||
}))
|
||||
}
|
||||
|
||||
async fn test_registry() -> PostgresRegistry {
|
||||
let database_url = env::var("TEST_DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgres://rmcp:rmcp@127.0.0.1:5432/rmcp".to_owned());
|
||||
let admin_pool = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let schema = format!(
|
||||
"test_mcp_server_{}_{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
);
|
||||
|
||||
admin_pool
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
PostgresRegistry::connect(&format!("{database_url}?options=-csearch_path%3D{schema}"))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn test_operation(base_url: &str) -> Operation<Schema, MappingSet> {
|
||||
Operation {
|
||||
id: OperationId::new("op_mcp_rest"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: base_url.to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/crm/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: object_schema("email"),
|
||||
output_schema: object_schema("id"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead".to_owned(),
|
||||
description: "Creates CRM lead".to_owned(),
|
||||
tags: Vec::new(),
|
||||
examples: Vec::new(),
|
||||
},
|
||||
samples: None,
|
||||
generated_draft: None,
|
||||
config_export: None,
|
||||
created_at: "2026-03-26T10:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-26T10:00:00Z".to_owned(),
|
||||
published_at: Some("2026-03-26T10:00:00Z".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn object_schema(field_name: &str) -> Schema {
|
||||
Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
field_name.to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use mcpaas_core::{HttpMethod, RestTarget};
|
||||
use reqwest::{
|
||||
Client,
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{RestAdapterError, RestRequest, RestResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RestAdapter {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Default for RestAdapter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RestAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
target: &RestTarget,
|
||||
request: &RestRequest,
|
||||
) -> Result<RestResponse, RestAdapterError> {
|
||||
let url = build_url(target, request)?;
|
||||
let headers = build_headers(target, request)?;
|
||||
let mut builder = self
|
||||
.client
|
||||
.request(to_reqwest_method(target.method), url)
|
||||
.headers(headers)
|
||||
.timeout(Duration::from_millis(request.timeout_ms));
|
||||
|
||||
if let Some(body) = &request.body {
|
||||
builder = builder.json(body);
|
||||
}
|
||||
|
||||
let response = builder.send().await?;
|
||||
let status = response.status();
|
||||
let headers = normalize_headers(response.headers());
|
||||
let body = decode_body(response).await?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(RestAdapterError::UnexpectedStatus {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(RestResponse {
|
||||
status_code: status.as_u16(),
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_url(target: &RestTarget, request: &RestRequest) -> Result<reqwest::Url, RestAdapterError> {
|
||||
let base_url =
|
||||
reqwest::Url::parse(&target.base_url).map_err(|_| RestAdapterError::InvalidBaseUrl {
|
||||
url: target.base_url.clone(),
|
||||
})?;
|
||||
let path = substitute_path_params(&target.path_template, &request.path_params);
|
||||
let mut url = base_url.join(path.trim_start_matches('/')).map_err(|_| {
|
||||
RestAdapterError::InvalidBaseUrl {
|
||||
url: target.base_url.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
{
|
||||
let mut query = url.query_pairs_mut();
|
||||
for (key, value) in &request.query_params {
|
||||
query.append_pair(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn substitute_path_params(path_template: &str, path_params: &BTreeMap<String, String>) -> String {
|
||||
let mut rendered = path_template.to_owned();
|
||||
|
||||
for (key, value) in path_params {
|
||||
rendered = rendered.replace(&format!("{{{key}}}"), value);
|
||||
}
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn build_headers(
|
||||
target: &RestTarget,
|
||||
request: &RestRequest,
|
||||
) -> Result<HeaderMap, RestAdapterError> {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
for (name, value) in &target.static_headers {
|
||||
insert_header(&mut headers, name, value)?;
|
||||
}
|
||||
|
||||
for (name, value) in &request.headers {
|
||||
insert_header(&mut headers, name, value)?;
|
||||
}
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn insert_header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<(), RestAdapterError> {
|
||||
let header_name =
|
||||
HeaderName::try_from(name).map_err(|_| RestAdapterError::InvalidHeaderName {
|
||||
header: name.to_owned(),
|
||||
})?;
|
||||
let header_value =
|
||||
HeaderValue::try_from(value).map_err(|_| RestAdapterError::InvalidHeaderValue {
|
||||
header: name.to_owned(),
|
||||
})?;
|
||||
|
||||
headers.insert(header_name, header_value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn decode_body(response: reqwest::Response) -> Result<Value, RestAdapterError> {
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
if bytes.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
|
||||
match serde_json::from_slice::<Value>(&bytes) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(Value::String(
|
||||
String::from_utf8_lossy(&bytes).trim().to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_headers(headers: &HeaderMap) -> BTreeMap<String, String> {
|
||||
headers
|
||||
.iter()
|
||||
.filter_map(|(name, value)| {
|
||||
value
|
||||
.to_str()
|
||||
.ok()
|
||||
.map(|value| (name.as_str().to_owned(), value.to_owned()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_reqwest_method(method: HttpMethod) -> reqwest::Method {
|
||||
match method {
|
||||
HttpMethod::Get => reqwest::Method::GET,
|
||||
HttpMethod::Post => reqwest::Method::POST,
|
||||
HttpMethod::Put => reqwest::Method::PUT,
|
||||
HttpMethod::Patch => reqwest::Method::PATCH,
|
||||
HttpMethod::Delete => reqwest::Method::DELETE,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query},
|
||||
http::HeaderMap,
|
||||
routing::{get, post},
|
||||
};
|
||||
use mcpaas_core::{HttpMethod, RestTarget};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{RestAdapter, RestAdapterError, RestRequest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_rest_request_and_normalizes_json_response() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/users/{user_id}".to_owned(),
|
||||
static_headers: BTreeMap::from([("x-static".to_owned(), "static".to_owned())]),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::from([("user_id".to_owned(), "42".to_owned())]),
|
||||
query_params: BTreeMap::from([("expand".to_owned(), "true".to_owned())]),
|
||||
headers: BTreeMap::from([("x-trace-id".to_owned(), "trace-123".to_owned())]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(
|
||||
response.body,
|
||||
json!({
|
||||
"id": "42",
|
||||
"query": "true",
|
||||
"trace": "trace-123",
|
||||
"static": "static",
|
||||
"payload": { "name": "Ada" }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_unexpected_status_with_normalized_body() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Get,
|
||||
path_template: "/fail".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::new(),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let error = adapter.execute(&target, &request).await.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
RestAdapterError::UnexpectedStatus {
|
||||
status: 502,
|
||||
body: Value::Object(_)
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
async fn spawn_test_server() -> String {
|
||||
let app = Router::new()
|
||||
.route("/users/{user_id}", post(create_user))
|
||||
.route("/fail", get(fail));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
Path(user_id): Path<String>,
|
||||
Query(query): Query<BTreeMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Json<Value> {
|
||||
let trace = headers
|
||||
.get("x-trace-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
let static_header = headers
|
||||
.get("x-static")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(json!({
|
||||
"id": user_id,
|
||||
"query": query.get("expand").cloned().unwrap_or_default(),
|
||||
"trace": trace,
|
||||
"static": static_header,
|
||||
"payload": payload
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fail() -> (axum::http::StatusCode, Json<Value>) {
|
||||
(
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "error": "upstream failed" })),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RestAdapterError {
|
||||
#[error("invalid base url: {url}")]
|
||||
InvalidBaseUrl { url: String },
|
||||
#[error("invalid path parameter {key}")]
|
||||
InvalidPathParameter { key: String },
|
||||
#[error("invalid query parameter {key}")]
|
||||
InvalidQueryParameter { key: String },
|
||||
#[error("invalid header name {header}")]
|
||||
InvalidHeaderName { header: String },
|
||||
#[error("invalid header value for {header}")]
|
||||
InvalidHeaderValue { header: String },
|
||||
#[error("request failed")]
|
||||
Transport(#[from] reqwest::Error),
|
||||
#[error("rest endpoint returned status {status}")]
|
||||
UnexpectedStatus { status: u16, body: Value },
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-rest"
|
||||
}
|
||||
mod client;
|
||||
mod error;
|
||||
mod model;
|
||||
|
||||
pub use client::RestAdapter;
|
||||
pub use error::RestAdapterError;
|
||||
pub use model::{RestRequest, RestResponse};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct RestRequest {
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub path_params: BTreeMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub query_params: BTreeMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<Value>,
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RestResponse {
|
||||
pub status_code: u16,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub body: Value,
|
||||
}
|
||||
@@ -16,3 +16,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use mcpaas_adapter_rest::RestAdapterError;
|
||||
use mcpaas_core::Protocol;
|
||||
use mcpaas_mapping::MappingError;
|
||||
use mcpaas_schema::SchemaError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeError {
|
||||
#[error(transparent)]
|
||||
Schema(#[from] SchemaError),
|
||||
#[error(transparent)]
|
||||
Mapping(#[from] MappingError),
|
||||
#[error(transparent)]
|
||||
RestAdapter(#[from] RestAdapterError),
|
||||
#[error("protocol {protocol:?} is not supported by runtime")]
|
||||
UnsupportedProtocol { protocol: Protocol },
|
||||
#[error("invalid prepared request: {details}")]
|
||||
InvalidPreparedRequest { details: String },
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use mcpaas_adapter_rest::{RestAdapter, RestRequest};
|
||||
use mcpaas_core::Target;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::{AdapterResponse, PreparedRequest, RuntimeError, RuntimeOperation};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeExecutor {
|
||||
rest_adapter: RestAdapter,
|
||||
}
|
||||
|
||||
impl Default for RuntimeExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rest_adapter: RestAdapter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
operation: &RuntimeOperation,
|
||||
input: &Value,
|
||||
) -> Result<Value, RuntimeError> {
|
||||
operation.input_schema.validate_shape(input)?;
|
||||
|
||||
let mapped_input = operation.input_mapping.apply(&json!({ "mcp": input }))?;
|
||||
let prepared_request = PreparedRequest::from_mapping_output(&mapped_input)?;
|
||||
|
||||
let adapter_response = match &operation.target {
|
||||
Target::Rest(target) => {
|
||||
let request = RestRequest {
|
||||
path_params: prepared_request.path_params.clone(),
|
||||
query_params: prepared_request.query_params.clone(),
|
||||
headers: merge_headers(
|
||||
&target.static_headers,
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
body: prepared_request.body.clone(),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.rest_adapter.execute(target, &request).await?;
|
||||
|
||||
AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: operation.protocol,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let finalized_output = finalize_output(operation, &adapter_response)?;
|
||||
|
||||
operation.output_schema.validate_shape(&finalized_output)?;
|
||||
|
||||
Ok(finalized_output)
|
||||
}
|
||||
}
|
||||
|
||||
impl PreparedRequest {
|
||||
pub fn from_mapping_output(mapped: &Value) -> Result<Self, RuntimeError> {
|
||||
let request =
|
||||
mapped
|
||||
.get("request")
|
||||
.ok_or_else(|| RuntimeError::InvalidPreparedRequest {
|
||||
details: "mapped input must contain request root".to_owned(),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
path_params: read_string_map(request.get("path"), "request.path")?,
|
||||
query_params: read_string_map(request.get("query"), "request.query")?,
|
||||
headers: read_string_map(request.get("headers"), "request.headers")?,
|
||||
body: request.get("body").and_then(non_empty_body).cloned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_output(
|
||||
operation: &RuntimeOperation,
|
||||
response: &AdapterResponse,
|
||||
) -> Result<Value, RuntimeError> {
|
||||
let mapped = operation.output_mapping.apply(&json!({
|
||||
"response": {
|
||||
"body": response.body,
|
||||
"data": response.body,
|
||||
"headers": response.headers,
|
||||
"status": response.status_code
|
||||
}
|
||||
}))?;
|
||||
|
||||
Ok(mapped
|
||||
.get("output")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())))
|
||||
}
|
||||
|
||||
fn merge_headers(
|
||||
static_headers: &BTreeMap<String, String>,
|
||||
execution_headers: &BTreeMap<String, String>,
|
||||
request_headers: &BTreeMap<String, String>,
|
||||
) -> BTreeMap<String, String> {
|
||||
let mut headers = static_headers.clone();
|
||||
headers.extend(execution_headers.clone());
|
||||
headers.extend(request_headers.clone());
|
||||
headers
|
||||
}
|
||||
|
||||
fn read_string_map(
|
||||
value: Option<&Value>,
|
||||
field_name: &str,
|
||||
) -> Result<BTreeMap<String, String>, RuntimeError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
|
||||
let Some(object) = value.as_object() else {
|
||||
return Err(RuntimeError::InvalidPreparedRequest {
|
||||
details: format!("{field_name} must be an object"),
|
||||
});
|
||||
};
|
||||
|
||||
object
|
||||
.iter()
|
||||
.map(|(key, value)| stringify_value(value).map(|value| (key.clone(), value)))
|
||||
.collect::<Result<BTreeMap<_, _>, _>>()
|
||||
.map_err(|details| RuntimeError::InvalidPreparedRequest { details })
|
||||
}
|
||||
|
||||
fn stringify_value(value: &Value) -> Result<String, String> {
|
||||
match value {
|
||||
Value::Null => Ok("null".to_owned()),
|
||||
Value::Bool(value) => Ok(value.to_string()),
|
||||
Value::Number(value) => Ok(value.to_string()),
|
||||
Value::String(value) => Ok(value.clone()),
|
||||
Value::Array(_) | Value::Object(_) => {
|
||||
Err("request path/query/headers accept only scalar values".to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_body(value: &Value) -> Option<&Value> {
|
||||
match value {
|
||||
Value::Null => None,
|
||||
Value::Object(object) if object.is_empty() => None,
|
||||
_ => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use mcpaas_core::{
|
||||
ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, HttpMethod, Operation, OperationId,
|
||||
OperationStatus, Protocol, RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
};
|
||||
use mcpaas_mapping::{MappingRule, MappingSet};
|
||||
use mcpaas_schema::{Schema, SchemaKind};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_rest_operation_end_to_end() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, false);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_input_shape() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, false);
|
||||
|
||||
let error = executor.execute(&operation, &json!({})).await.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn propagates_external_rest_errors() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, true, false);
|
||||
|
||||
let error = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::RestAdapter(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_output_mapping_requires_missing_field() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, true);
|
||||
|
||||
let error = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::Mapping(_)));
|
||||
}
|
||||
|
||||
async fn spawn_runtime_server() -> String {
|
||||
let app = Router::new().route("/leads", post(create_lead));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_lead(Json(payload): Json<Value>) -> (axum::http::StatusCode, Json<Value>) {
|
||||
let should_fail = payload
|
||||
.get("fail")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_fail {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "error": "upstream failed" })),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
Json(json!({ "id": "lead_123", "email": payload["email"] })),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_rest_operation(
|
||||
base_url: &str,
|
||||
should_fail: bool,
|
||||
invalid_output: bool,
|
||||
) -> RuntimeOperation {
|
||||
let mut input_rules = vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}];
|
||||
|
||||
if should_fail {
|
||||
input_rules.push(MappingRule {
|
||||
source: "$.mcp.fail".to_owned(),
|
||||
target: "$.request.body.fail".to_owned(),
|
||||
required: false,
|
||||
default_value: Some(Value::Bool(true)),
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
});
|
||||
}
|
||||
|
||||
let output_source = if invalid_output {
|
||||
"$.response.body.missing"
|
||||
} else {
|
||||
"$.response.body.id"
|
||||
};
|
||||
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_rest_runtime"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: base_url.to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: object_schema("email", SchemaKind::String),
|
||||
output_schema: object_schema("id", SchemaKind::String),
|
||||
input_mapping: MappingSet { rules: input_rules },
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: output_source.to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead".to_owned(),
|
||||
description: "Creates a CRM lead".to_owned(),
|
||||
tags: vec!["crm".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "email": "user@example.com" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["input_json".to_owned()],
|
||||
generated_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn object_schema(field_name: &str, kind: SchemaKind) -> Schema {
|
||||
Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
field_name.to_owned(),
|
||||
Schema {
|
||||
kind,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-runtime"
|
||||
}
|
||||
mod error;
|
||||
mod executor;
|
||||
mod model;
|
||||
|
||||
pub use error::RuntimeError;
|
||||
pub use executor::RuntimeExecutor;
|
||||
pub use model::{AdapterResponse, PreparedRequest, RuntimeOperation};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use mcpaas_core::{ExecutionConfig, Operation, OperationId, Protocol, Target, ToolDescription};
|
||||
use mcpaas_mapping::MappingSet;
|
||||
use mcpaas_schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeOperation {
|
||||
pub operation_id: OperationId,
|
||||
pub tool_name: String,
|
||||
pub protocol: Protocol,
|
||||
pub target: Target,
|
||||
pub input_schema: Schema,
|
||||
pub output_schema: Schema,
|
||||
pub input_mapping: MappingSet,
|
||||
pub output_mapping: MappingSet,
|
||||
pub execution_config: ExecutionConfig,
|
||||
pub tool_description: ToolDescription,
|
||||
}
|
||||
|
||||
impl From<Operation<Schema, MappingSet>> for RuntimeOperation {
|
||||
fn from(value: Operation<Schema, MappingSet>) -> Self {
|
||||
Self {
|
||||
operation_id: value.id,
|
||||
tool_name: value.name,
|
||||
protocol: value.protocol,
|
||||
target: value.target,
|
||||
input_schema: value.input_schema,
|
||||
output_schema: value.output_schema,
|
||||
input_mapping: value.input_mapping,
|
||||
output_mapping: value.output_mapping,
|
||||
execution_config: value.execution_config,
|
||||
tool_description: value.tool_description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct PreparedRequest {
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub path_params: BTreeMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub query_params: BTreeMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AdapterResponse {
|
||||
pub status_code: u16,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub body: Value,
|
||||
}
|
||||
Reference in New Issue
Block a user