feat: add grpc support
This commit is contained in:
Generated
+436
@@ -7,8 +7,10 @@ name = "admin-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mcpaas-adapter-grpc",
|
||||
"mcpaas-core",
|
||||
"mcpaas-mapping",
|
||||
"mcpaas-proto",
|
||||
"mcpaas-registry",
|
||||
"mcpaas-runtime",
|
||||
"mcpaas-schema",
|
||||
@@ -46,6 +48,17 @@ 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]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@@ -290,6 +303,16 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
@@ -312,12 +335,30 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[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"
|
||||
@@ -443,6 +484,25 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -563,6 +623,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -592,6 +653,19 @@ dependencies = [
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-timeout"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||
dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -751,6 +825,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -797,6 +880,12 @@ dependencies = [
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
@@ -844,6 +933,7 @@ name = "mcp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mcpaas-adapter-grpc",
|
||||
"mcpaas-core",
|
||||
"mcpaas-mapping",
|
||||
"mcpaas-registry",
|
||||
@@ -877,11 +967,20 @@ dependencies = [
|
||||
name = "mcpaas-adapter-grpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"mcpaas-core",
|
||||
"mcpaas-proto",
|
||||
"prost",
|
||||
"prost-reflect",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -924,6 +1023,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"mcpaas-schema",
|
||||
"prost",
|
||||
"prost-reflect",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1003,6 +1105,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -1033,6 +1141,15 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1068,6 +1185,37 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
@@ -1129,6 +1277,156 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark-to-cmark",
|
||||
"regex",
|
||||
"syn",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde-value",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa"
|
||||
dependencies = [
|
||||
"protoc-bin-vendored-linux-aarch_64",
|
||||
"protoc-bin-vendored-linux-ppcle_64",
|
||||
"protoc-bin-vendored-linux-s390_64",
|
||||
"protoc-bin-vendored-linux-x86_32",
|
||||
"protoc-bin-vendored-linux-x86_64",
|
||||
"protoc-bin-vendored-macos-aarch_64",
|
||||
"protoc-bin-vendored-macos-x86_64",
|
||||
"protoc-bin-vendored-win32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-aarch_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-s390_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_32"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-aarch_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-x86_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-win32"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-to-cmark"
|
||||
version = "22.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90"
|
||||
dependencies = [
|
||||
"pulldown-cmark",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -1282,6 +1580,18 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
@@ -1357,6 +1667,19 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
@@ -1426,6 +1749,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
@@ -1720,6 +2053,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
@@ -1852,6 +2198,87 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64",
|
||||
"bytes",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"socket2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost-build"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"tonic-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -1860,9 +2287,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -1972,6 +2402,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
|
||||
@@ -22,6 +22,11 @@ version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = "0.8"
|
||||
base64 = "0.22"
|
||||
prost = "0.14"
|
||||
prost-reflect = { version = "0.16", features = ["serde"] }
|
||||
prost-types = "0.14"
|
||||
protoc-bin-vendored = "3"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -30,6 +35,10 @@ sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-r
|
||||
thiserror = "2"
|
||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
tonic = { version = "0.14", features = ["transport"] }
|
||||
tonic-prost = "0.14"
|
||||
tonic-build = "0.14"
|
||||
tonic-prost-build = "0.14"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
uuid = { version = "1", features = ["serde", "v7"] }
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/graphql-support`
|
||||
### `feat/grpc-support`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
|
||||
- GraphQL operation can be created, tested and published
|
||||
- variables mapping and `response_path` work in runtime
|
||||
- GraphQL `errors` are translated into runtime failures
|
||||
- published GraphQL tool is callable through MCP
|
||||
- integration tests cover admin-api, runtime and MCP GraphQL scenarios
|
||||
- gRPC descriptor set can be uploaded and inspected through admin-api
|
||||
- unary gRPC operation can be tested through runtime
|
||||
- published gRPC tool is callable through MCP
|
||||
- integration tests cover adapter, runtime, admin-api and MCP gRPC scenarios
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/grpc-support`
|
||||
- `feat/hardening`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ version.workspace = true
|
||||
axum.workspace = true
|
||||
mcpaas-core = { path = "../../crates/mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../../crates/mcpaas-mapping" }
|
||||
mcpaas-proto = { path = "../../crates/mcpaas-proto" }
|
||||
mcpaas-registry = { path = "../../crates/mcpaas-registry" }
|
||||
mcpaas-runtime = { path = "../../crates/mcpaas-runtime" }
|
||||
mcpaas-schema = { path = "../../crates/mcpaas-schema" }
|
||||
@@ -23,5 +24,6 @@ tracing-subscriber.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
mcpaas-adapter-grpc = { path = "../../crates/mcpaas-adapter-grpc", features = ["test-support"] }
|
||||
reqwest.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
||||
+170
-4
@@ -8,8 +8,9 @@ use crate::{
|
||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||
operations::{
|
||||
create_operation, create_version, export_operation, generate_draft, get_operation,
|
||||
get_operation_version, list_operations, publish_operation, run_test, upload_input_json,
|
||||
upload_output_json,
|
||||
get_operation_version, list_grpc_services, list_operations, publish_operation,
|
||||
run_test, upload_descriptor_set, upload_input_json, upload_output_json,
|
||||
upload_proto_descriptor,
|
||||
},
|
||||
},
|
||||
state::AppState,
|
||||
@@ -38,6 +39,18 @@ pub fn build_app(state: AppState) -> Router {
|
||||
"/operations/{operation_id}/samples/output-json",
|
||||
post(upload_output_json),
|
||||
)
|
||||
.route(
|
||||
"/operations/{operation_id}/descriptors/proto",
|
||||
post(upload_proto_descriptor),
|
||||
)
|
||||
.route(
|
||||
"/operations/{operation_id}/descriptors/descriptor-set",
|
||||
post(upload_descriptor_set),
|
||||
)
|
||||
.route(
|
||||
"/operations/{operation_id}/grpc/services",
|
||||
get(list_grpc_services),
|
||||
)
|
||||
.route(
|
||||
"/operations/{operation_id}/drafts/generate",
|
||||
post(generate_draft),
|
||||
@@ -67,9 +80,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use mcpaas_adapter_grpc::test_support as grpc_test_support;
|
||||
use mcpaas_core::{
|
||||
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, Protocol, RestTarget,
|
||||
Target, ToolDescription,
|
||||
DescriptorId, ExecutionConfig, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod,
|
||||
Protocol, RestTarget, Target, ToolDescription,
|
||||
};
|
||||
use mcpaas_mapping::{MappingRule, MappingSet};
|
||||
use mcpaas_registry::PostgresRegistry;
|
||||
@@ -199,6 +213,105 @@ mod tests {
|
||||
assert_eq!(test_run["response_preview"]["id"], "lead_123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uploads_descriptor_set_and_lists_grpc_services() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("grpc_descriptor");
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_grpc_operation_payload(
|
||||
&server_addr,
|
||||
"echo_descriptor",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let uploaded = client
|
||||
.post(format!(
|
||||
"{base_url}/operations/{operation_id}/descriptors/descriptor-set"
|
||||
))
|
||||
.header("x-file-name", "echo_descriptor.bin")
|
||||
.body(grpc_test_support::echo::FILE_DESCRIPTOR_SET.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let services = client
|
||||
.get(format!(
|
||||
"{base_url}/operations/{operation_id}/grpc/services"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(uploaded["descriptor_id"].as_str().is_some());
|
||||
assert_eq!(services["services"][0]["package"], "echo");
|
||||
assert_eq!(services["services"][0]["service"], "EchoService");
|
||||
assert_eq!(services["services"][0]["methods"][0]["name"], "UnaryEcho");
|
||||
assert_eq!(services["services"][0]["methods"][0]["kind"], "unary");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_publishes_and_tests_grpc_operation() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("grpc_runtime");
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_grpc_operation_payload(&server_addr, "echo_runtime"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let published = client
|
||||
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
||||
.json(&json!({ "version": 1 }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let test_run = client
|
||||
.post(format!("{base_url}/operations/{operation_id}/test-runs"))
|
||||
.json(&json!({
|
||||
"version": 1,
|
||||
"input": { "message": "hello" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(published["published_version"], 1);
|
||||
assert_eq!(test_run["ok"], true);
|
||||
assert_eq!(test_run["request_preview"]["grpc"]["message"], "hello");
|
||||
assert_eq!(test_run["response_preview"]["message"], "hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_auth_profiles_and_yaml_upsert() {
|
||||
let registry = test_registry().await;
|
||||
@@ -536,6 +649,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_grpc_operation_payload(server_addr: &str, name: &str) -> OperationPayload {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
package: "echo".to_owned(),
|
||||
service: "EchoService".to_owned(),
|
||||
method: "UnaryEcho".to_owned(),
|
||||
descriptor_ref: DescriptorId::new("desc_echo"),
|
||||
descriptor_set_b64: grpc_test_support::descriptor_set_b64(),
|
||||
}),
|
||||
input_schema: object_schema("message"),
|
||||
output_schema: object_schema("message"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.message".to_owned(),
|
||||
target: "$.request.grpc.message".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.message".to_owned(),
|
||||
target: "$.output.message".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: "Unary Echo gRPC".to_owned(),
|
||||
description: "Echoes a unary gRPC payload".to_owned(),
|
||||
tags: vec!["grpc".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn object_schema(field_name: &str) -> Schema {
|
||||
Schema {
|
||||
kind: SchemaKind::Object,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use axum::{
|
||||
Json,
|
||||
body::Bytes,
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode, header},
|
||||
response::IntoResponse,
|
||||
@@ -128,6 +129,56 @@ pub async fn upload_output_json(
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn upload_proto_descriptor(
|
||||
Path(operation_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let source_name = header_file_name(&headers);
|
||||
let descriptor = state
|
||||
.service
|
||||
.upload_proto_file(
|
||||
&operation_id.as_str().into(),
|
||||
source_name.as_deref(),
|
||||
body.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!(descriptor)))
|
||||
}
|
||||
|
||||
pub async fn upload_descriptor_set(
|
||||
Path(operation_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let source_name = header_file_name(&headers);
|
||||
let descriptor = state
|
||||
.service
|
||||
.upload_descriptor_set(
|
||||
&operation_id.as_str().into(),
|
||||
source_name.as_deref(),
|
||||
body.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!(descriptor)))
|
||||
}
|
||||
|
||||
pub async fn list_grpc_services(
|
||||
Path(operation_id): Path<String>,
|
||||
Query(query): Query<ExportQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let services = state
|
||||
.service
|
||||
.list_grpc_services(&operation_id.as_str().into(), query.version)
|
||||
.await?;
|
||||
Ok(Json(json!({ "services": services })))
|
||||
}
|
||||
|
||||
pub async fn generate_draft(
|
||||
Path(operation_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
@@ -166,3 +217,10 @@ pub async fn import_operation(
|
||||
let imported = state.service.import_operation(query, &body).await?;
|
||||
Ok(Json(json!(imported)))
|
||||
}
|
||||
|
||||
fn header_file_name(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("x-file-name")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ use mcpaas_core::{
|
||||
GeneratedDraftStatus, OperationId, OperationStatus, Protocol, SampleId, Samples, Target,
|
||||
};
|
||||
use mcpaas_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use mcpaas_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||
use mcpaas_registry::{
|
||||
CreateVersionRequest, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PostgresRegistry, PublishRequest, RegistryOperation, SampleKind, SaveAuthProfileRequest,
|
||||
SaveSampleMetadataRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
};
|
||||
use mcpaas_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
use mcpaas_schema::Schema;
|
||||
@@ -139,6 +140,27 @@ pub struct ImportResponse {
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct DescriptorUploadResponse {
|
||||
pub descriptor_id: String,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct GrpcServiceSummary {
|
||||
pub package: String,
|
||||
pub service: String,
|
||||
pub methods: Vec<GrpcMethodSummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct GrpcMethodSummary {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub input_schema: Schema,
|
||||
pub output_schema: Schema,
|
||||
}
|
||||
|
||||
impl AdminService {
|
||||
pub fn new(registry: PostgresRegistry, storage_root: PathBuf) -> Self {
|
||||
Self {
|
||||
@@ -345,6 +367,138 @@ impl AdminService {
|
||||
Ok(self.registry.list_auth_profiles().await?)
|
||||
}
|
||||
|
||||
pub async fn upload_descriptor_set(
|
||||
&self,
|
||||
operation_id: &OperationId,
|
||||
source_name: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<DescriptorUploadResponse, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"descriptor upload is only allowed for grpc operations",
|
||||
));
|
||||
}
|
||||
|
||||
let services = services_from_descriptor_set_bytes(payload)
|
||||
.map_err(|error| ApiError::validation(error.to_string()))?;
|
||||
let descriptor_id = mcpaas_core::DescriptorId::new(new_prefixed_id("desc"));
|
||||
let storage_ref = self
|
||||
.storage
|
||||
.write_descriptor(
|
||||
operation_id,
|
||||
summary.current_draft_version,
|
||||
mcpaas_registry::DescriptorKind::DescriptorSet,
|
||||
&descriptor_id,
|
||||
source_name,
|
||||
payload,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.registry
|
||||
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
|
||||
descriptor: &mcpaas_registry::DescriptorMetadata {
|
||||
id: descriptor_id.clone(),
|
||||
operation_id: Some(operation_id.clone()),
|
||||
version: Some(summary.current_draft_version),
|
||||
descriptor_kind: mcpaas_registry::DescriptorKind::DescriptorSet,
|
||||
storage_ref,
|
||||
source_name: source_name.map(ToOwned::to_owned),
|
||||
package_index: Some(
|
||||
serde_json::to_value(&services)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))?,
|
||||
),
|
||||
created_at: now_string()?,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(DescriptorUploadResponse {
|
||||
descriptor_id: descriptor_id.as_str().to_owned(),
|
||||
version: summary.current_draft_version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn upload_proto_file(
|
||||
&self,
|
||||
operation_id: &OperationId,
|
||||
source_name: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<DescriptorUploadResponse, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"proto upload is only allowed for grpc operations",
|
||||
));
|
||||
}
|
||||
|
||||
let descriptor_id = mcpaas_core::DescriptorId::new(new_prefixed_id("desc"));
|
||||
let storage_ref = self
|
||||
.storage
|
||||
.write_descriptor(
|
||||
operation_id,
|
||||
summary.current_draft_version,
|
||||
mcpaas_registry::DescriptorKind::ProtoUpload,
|
||||
&descriptor_id,
|
||||
source_name,
|
||||
payload,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.registry
|
||||
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
|
||||
descriptor: &mcpaas_registry::DescriptorMetadata {
|
||||
id: descriptor_id.clone(),
|
||||
operation_id: Some(operation_id.clone()),
|
||||
version: Some(summary.current_draft_version),
|
||||
descriptor_kind: mcpaas_registry::DescriptorKind::ProtoUpload,
|
||||
storage_ref,
|
||||
source_name: source_name.map(ToOwned::to_owned),
|
||||
package_index: None,
|
||||
created_at: now_string()?,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(DescriptorUploadResponse {
|
||||
descriptor_id: descriptor_id.as_str().to_owned(),
|
||||
version: summary.current_draft_version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_grpc_services(
|
||||
&self,
|
||||
operation_id: &OperationId,
|
||||
version: Option<u32>,
|
||||
) -> Result<Vec<GrpcServiceSummary>, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"grpc services are only available for grpc operations",
|
||||
));
|
||||
}
|
||||
|
||||
let version = version.unwrap_or(summary.current_draft_version);
|
||||
let descriptor = self
|
||||
.registry
|
||||
.list_descriptor_metadata(operation_id, version)
|
||||
.await?
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|descriptor| {
|
||||
descriptor.descriptor_kind == mcpaas_registry::DescriptorKind::DescriptorSet
|
||||
})
|
||||
.ok_or_else(|| ApiError::not_found("descriptor set was not uploaded"))?;
|
||||
let bytes = self.storage.read_bytes(&descriptor.storage_ref).await?;
|
||||
let services = services_from_descriptor_set_bytes(&bytes)
|
||||
.map_err(|error| ApiError::validation(error.to_string()))?;
|
||||
|
||||
services
|
||||
.into_iter()
|
||||
.map(proto_service_summary)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
pub async fn get_auth_profile(
|
||||
&self,
|
||||
auth_profile_id: &AuthProfileId,
|
||||
@@ -612,6 +766,7 @@ fn build_request_preview(
|
||||
"path": prepared.path_params,
|
||||
"query": prepared.query_params,
|
||||
"headers": prepared.headers,
|
||||
"grpc": prepared.grpc.unwrap_or(Value::Null),
|
||||
"variables": prepared.variables.unwrap_or(Value::Null),
|
||||
"body": prepared.body.unwrap_or(Value::Null)
|
||||
}))
|
||||
@@ -672,3 +827,25 @@ fn now_string() -> Result<String, ApiError> {
|
||||
.format(&Rfc3339)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))
|
||||
}
|
||||
|
||||
fn proto_service_summary(service: ProtoService) -> Result<GrpcServiceSummary, ApiError> {
|
||||
let methods = service
|
||||
.unary_methods()
|
||||
.map(|method| {
|
||||
Ok(GrpcMethodSummary {
|
||||
name: method.name.clone(),
|
||||
kind: "unary".to_owned(),
|
||||
input_schema: mcpaas_proto::message_to_schema(&method.input)
|
||||
.map_err(|error| ApiError::validation(error.to_string()))?,
|
||||
output_schema: mcpaas_proto::message_to_schema(&method.output)
|
||||
.map_err(|error| ApiError::validation(error.to_string()))?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, ApiError>>()?;
|
||||
|
||||
Ok(GrpcServiceSummary {
|
||||
package: service.package,
|
||||
service: service.name,
|
||||
methods,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use mcpaas_core::{OperationId, SampleId};
|
||||
use mcpaas_registry::SampleKind;
|
||||
use mcpaas_core::{DescriptorId, OperationId, SampleId};
|
||||
use mcpaas_registry::{DescriptorKind, SampleKind};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
@@ -57,6 +57,39 @@ impl LocalArtifactStorage {
|
||||
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
}
|
||||
|
||||
pub async fn write_descriptor(
|
||||
&self,
|
||||
operation_id: &OperationId,
|
||||
version: u32,
|
||||
descriptor_kind: DescriptorKind,
|
||||
descriptor_id: &DescriptorId,
|
||||
source_name: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<String, StorageError> {
|
||||
let file_name = match descriptor_kind {
|
||||
DescriptorKind::ProtoUpload => source_name.unwrap_or("schema.proto").to_owned(),
|
||||
DescriptorKind::DescriptorSet => source_name.unwrap_or("descriptor-set.bin").to_owned(),
|
||||
DescriptorKind::ReflectionSnapshot => {
|
||||
source_name.unwrap_or("reflection.bin").to_owned()
|
||||
}
|
||||
};
|
||||
let path = self
|
||||
.root
|
||||
.join("descriptors")
|
||||
.join(operation_id.as_str())
|
||||
.join(format!("v{version}"))
|
||||
.join(format!("{}_{}", descriptor_id.as_str(), file_name));
|
||||
|
||||
write_bytes(&path, payload).await?;
|
||||
|
||||
Ok(to_storage_ref(&path))
|
||||
}
|
||||
|
||||
pub async fn read_bytes(&self, storage_ref: &str) -> Result<Vec<u8>, StorageError> {
|
||||
let path = from_storage_ref(storage_ref)?;
|
||||
Ok(fs::read(path).await?)
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_json_file(path: &Path, payload: &Value) -> Result<(), StorageError> {
|
||||
@@ -69,6 +102,15 @@ async fn write_json_file(path: &Path, payload: &Value) -> Result<(), StorageErro
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_bytes(path: &Path, payload: &[u8]) -> Result<(), StorageError> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
fs::write(path, payload).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_storage_ref(path: &Path) -> String {
|
||||
format!("file://{}", path.display())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ tracing-subscriber.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
mcpaas-adapter-grpc = { path = "../../crates/mcpaas-adapter-grpc", features = ["test-support"] }
|
||||
mcpaas-core = { path = "../../crates/mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../../crates/mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../../crates/mcpaas-schema" }
|
||||
|
||||
@@ -437,6 +437,7 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str {
|
||||
RuntimeError::Schema(_) => "schema_validation_error",
|
||||
RuntimeError::Mapping(_) => "mapping_error",
|
||||
RuntimeError::GraphqlAdapter(_) => "adapter_execution_error",
|
||||
RuntimeError::GrpcAdapter(_) => "adapter_execution_error",
|
||||
RuntimeError::RestAdapter(_) => "adapter_execution_error",
|
||||
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
|
||||
RuntimeError::InvalidPreparedRequest { .. } => "runtime_error",
|
||||
|
||||
+119
-2
@@ -49,9 +49,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use axum::{Json, Router, http::header, routing::post};
|
||||
use mcpaas_adapter_grpc::test_support as grpc_test_support;
|
||||
use mcpaas_core::{
|
||||
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, Operation, OperationId,
|
||||
OperationStatus, Protocol, RestTarget, Target, ToolDescription,
|
||||
DescriptorId, ExecutionConfig, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod,
|
||||
Operation, OperationId, OperationStatus, Protocol, RestTarget, Target, ToolDescription,
|
||||
};
|
||||
use mcpaas_mapping::{MappingRule, MappingSet};
|
||||
use mcpaas_registry::{PostgresRegistry, PublishRequest};
|
||||
@@ -183,6 +184,60 @@ mod tests {
|
||||
assert_eq!(call_result["result"]["isError"], false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initializes_and_calls_published_grpc_tool_via_mcp() {
|
||||
let registry = test_registry().await;
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
let operation = test_grpc_operation(&server_addr, "echo_unary_grpc");
|
||||
|
||||
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 base_url = spawn_mcp_server(build_app(
|
||||
registry,
|
||||
Duration::from_millis(0),
|
||||
Some("https://rmcp.example.com".to_owned()),
|
||||
))
|
||||
.await;
|
||||
let client = reqwest::Client::new();
|
||||
let initialized_session = initialize_session(&client, &base_url).await;
|
||||
|
||||
let call_result = post_jsonrpc(
|
||||
&client,
|
||||
&base_url,
|
||||
Some(&initialized_session),
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "echo_unary_grpc",
|
||||
"arguments": {
|
||||
"message": "hello"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
call_result["result"]["structuredContent"],
|
||||
json!({ "message": "hello" })
|
||||
);
|
||||
assert_eq!(call_result["result"]["isError"], false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requires_initialized_notification_before_tool_methods() {
|
||||
let registry = test_registry().await;
|
||||
@@ -572,6 +627,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_grpc_operation(server_addr: &str, name: &str) -> Operation<Schema, MappingSet> {
|
||||
Operation {
|
||||
id: OperationId::new(format!("op_{name}")),
|
||||
name: name.to_owned(),
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
package: "echo".to_owned(),
|
||||
service: "EchoService".to_owned(),
|
||||
method: "UnaryEcho".to_owned(),
|
||||
descriptor_ref: DescriptorId::new("desc_echo"),
|
||||
descriptor_set_b64: grpc_test_support::descriptor_set_b64(),
|
||||
}),
|
||||
input_schema: object_schema("message"),
|
||||
output_schema: object_schema("message"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.message".to_owned(),
|
||||
target: "$.request.grpc.message".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.message".to_owned(),
|
||||
target: "$.output.message".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: "Unary Echo gRPC".to_owned(),
|
||||
description: "Echoes a unary gRPC payload".to_owned(),
|
||||
tags: vec!["grpc".to_owned()],
|
||||
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,
|
||||
|
||||
@@ -5,10 +5,25 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-proto = { path = "../mcpaas-proto" }
|
||||
prost.workspace = true
|
||||
prost-reflect.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tonic.workspace = true
|
||||
tonic-prost.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
prost-types.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
protoc-bin-vendored.workspace = true
|
||||
tonic-prost-build.workspace = true
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("vendored protoc must exist");
|
||||
unsafe {
|
||||
env::set_var("PROTOC", protoc);
|
||||
}
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must exist"));
|
||||
let descriptor_path = out_dir.join("echo_descriptor.bin");
|
||||
|
||||
tonic_prost_build::configure()
|
||||
.build_server(true)
|
||||
.build_client(true)
|
||||
.file_descriptor_set_path(descriptor_path)
|
||||
.compile_protos(&["proto/echo.proto"], &["proto"])
|
||||
.expect("echo proto must compile");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package echo;
|
||||
|
||||
service EchoService {
|
||||
rpc UnaryEcho(EchoRequest) returns (EchoResponse);
|
||||
}
|
||||
|
||||
message EchoRequest {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message EchoResponse {
|
||||
string message = 1;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
use std::{collections::BTreeMap, str::FromStr, time::Duration};
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use mcpaas_core::GrpcTarget;
|
||||
use prost::Message;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor, prost_types::FileDescriptorSet};
|
||||
use tonic::{
|
||||
Request,
|
||||
client::Grpc,
|
||||
metadata::{KeyAndValueRef, MetadataKey, MetadataValue},
|
||||
transport::Endpoint,
|
||||
};
|
||||
|
||||
use crate::{GrpcAdapterError, GrpcRequest, GrpcResponse, codec::JsonCodec};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GrpcAdapter;
|
||||
|
||||
impl GrpcAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
target: &GrpcTarget,
|
||||
request: &GrpcRequest,
|
||||
) -> Result<GrpcResponse, GrpcAdapterError> {
|
||||
let method = resolve_method(target)?;
|
||||
if method.is_client_streaming() || method.is_server_streaming() {
|
||||
return Err(GrpcAdapterError::UnsupportedMethodKind {
|
||||
service: format!("{}.{}", target.package, target.service),
|
||||
method: target.method.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let endpoint = Endpoint::from_shared(target.server_addr.clone())?
|
||||
.timeout(Duration::from_millis(request.timeout_ms));
|
||||
let channel = endpoint.connect().await?;
|
||||
let mut grpc = Grpc::new(channel);
|
||||
grpc.ready()
|
||||
.await
|
||||
.map_err(|error| GrpcAdapterError::Status {
|
||||
code: tonic::Code::Unavailable,
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let service_name = method.parent_service().full_name().to_owned();
|
||||
let method_name = method.name().to_owned();
|
||||
let path = tonic::codegen::http::uri::PathAndQuery::from_str(&format!(
|
||||
"/{service_name}/{method_name}"
|
||||
))
|
||||
.map_err(|_| GrpcAdapterError::InvalidMethodPath {
|
||||
service: service_name,
|
||||
method: method_name,
|
||||
})?;
|
||||
let codec = JsonCodec::new(method.input(), method.output());
|
||||
let mut tonic_request = Request::new(request.body.clone());
|
||||
|
||||
apply_headers(&mut tonic_request, &request.headers)?;
|
||||
|
||||
let response = grpc.unary(tonic_request, path, codec).await?;
|
||||
let headers = normalize_headers(response.metadata());
|
||||
let body = response.into_inner();
|
||||
|
||||
Ok(GrpcResponse {
|
||||
status_code: 200,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_method(target: &GrpcTarget) -> Result<MethodDescriptor, GrpcAdapterError> {
|
||||
let bytes = STANDARD
|
||||
.decode(&target.descriptor_set_b64)
|
||||
.map_err(|_| GrpcAdapterError::InvalidDescriptorEncoding)?;
|
||||
let descriptor_set = FileDescriptorSet::decode(bytes.as_slice())
|
||||
.map_err(|_| GrpcAdapterError::InvalidDescriptorSet)?;
|
||||
let pool = DescriptorPool::from_file_descriptor_set(descriptor_set)
|
||||
.map_err(|_| GrpcAdapterError::InvalidDescriptorPool)?;
|
||||
let full_service_name = if target.package.is_empty() {
|
||||
target.service.clone()
|
||||
} else {
|
||||
format!("{}.{}", target.package, target.service)
|
||||
};
|
||||
let service = pool
|
||||
.get_service_by_name(&full_service_name)
|
||||
.ok_or_else(|| GrpcAdapterError::ServiceNotFound {
|
||||
service: full_service_name.clone(),
|
||||
})?;
|
||||
|
||||
service
|
||||
.methods()
|
||||
.find(|method| method.name() == target.method)
|
||||
.ok_or_else(|| GrpcAdapterError::MethodNotFound {
|
||||
service: full_service_name,
|
||||
method: target.method.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_headers(
|
||||
request: &mut Request<serde_json::Value>,
|
||||
headers: &BTreeMap<String, String>,
|
||||
) -> Result<(), GrpcAdapterError> {
|
||||
for (key, value) in headers {
|
||||
let metadata_key =
|
||||
MetadataKey::from_str(key).map_err(|source| GrpcAdapterError::InvalidMetadataKey {
|
||||
key: key.clone(),
|
||||
source,
|
||||
})?;
|
||||
let metadata_value = MetadataValue::from_str(value).map_err(|source| {
|
||||
GrpcAdapterError::InvalidMetadataValue {
|
||||
key: key.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
request.metadata_mut().insert(metadata_key, metadata_value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_headers(metadata: &tonic::metadata::MetadataMap) -> BTreeMap<String, String> {
|
||||
metadata
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
KeyAndValueRef::Ascii(key, value) => {
|
||||
Some((key.as_str().to_owned(), value.to_str().ok()?.to_owned()))
|
||||
}
|
||||
KeyAndValueRef::Binary(_, _) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use mcpaas_core::{DescriptorId, GrpcTarget};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{GrpcAdapter, GrpcRequest, test_support};
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_unary_grpc_request() {
|
||||
let server_addr = test_support::spawn_unary_echo_server().await;
|
||||
let adapter = GrpcAdapter::new();
|
||||
let target = GrpcTarget {
|
||||
server_addr,
|
||||
package: "echo".to_owned(),
|
||||
service: "EchoService".to_owned(),
|
||||
method: "UnaryEcho".to_owned(),
|
||||
descriptor_ref: DescriptorId::new("desc_echo"),
|
||||
descriptor_set_b64: test_support::descriptor_set_b64(),
|
||||
};
|
||||
let request = GrpcRequest {
|
||||
headers: BTreeMap::new(),
|
||||
body: json!({ "message": "hello" }),
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
|
||||
assert_eq!(response.body, json!({ "message": "hello" }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use prost::Message;
|
||||
use prost_reflect::{DynamicMessage, MessageDescriptor};
|
||||
use tonic::{
|
||||
Status,
|
||||
codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder},
|
||||
};
|
||||
|
||||
pub struct JsonCodec {
|
||||
request_descriptor: MessageDescriptor,
|
||||
response_descriptor: MessageDescriptor,
|
||||
}
|
||||
|
||||
impl JsonCodec {
|
||||
pub fn new(
|
||||
request_descriptor: MessageDescriptor,
|
||||
response_descriptor: MessageDescriptor,
|
||||
) -> Self {
|
||||
Self {
|
||||
request_descriptor,
|
||||
response_descriptor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec for JsonCodec {
|
||||
type Encode = serde_json::Value;
|
||||
type Decode = serde_json::Value;
|
||||
|
||||
type Encoder = JsonEncoder;
|
||||
type Decoder = JsonDecoder;
|
||||
|
||||
fn encoder(&mut self) -> Self::Encoder {
|
||||
JsonEncoder(self.request_descriptor.clone())
|
||||
}
|
||||
|
||||
fn decoder(&mut self) -> Self::Decoder {
|
||||
JsonDecoder(self.response_descriptor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JsonEncoder(MessageDescriptor);
|
||||
|
||||
impl Encoder for JsonEncoder {
|
||||
type Item = serde_json::Value;
|
||||
type Error = Status;
|
||||
|
||||
fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
|
||||
let message = DynamicMessage::deserialize(self.0.clone(), item)
|
||||
.map_err(|error| Status::invalid_argument(error.to_string()))?;
|
||||
|
||||
message.encode_raw(dst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JsonDecoder(MessageDescriptor);
|
||||
|
||||
impl Decoder for JsonDecoder {
|
||||
type Item = serde_json::Value;
|
||||
type Error = Status;
|
||||
|
||||
fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
|
||||
let mut message = DynamicMessage::new(self.0.clone());
|
||||
message
|
||||
.merge(src)
|
||||
.map_err(|error| Status::internal(error.to_string()))?;
|
||||
|
||||
serde_json::to_value(&message)
|
||||
.map(Some)
|
||||
.map_err(|error| Status::internal(error.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use thiserror::Error;
|
||||
use tonic::{
|
||||
Code, Status,
|
||||
metadata::errors::{InvalidMetadataKey, InvalidMetadataValue},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GrpcAdapterError {
|
||||
#[error("invalid descriptor set encoding")]
|
||||
InvalidDescriptorEncoding,
|
||||
#[error("invalid descriptor set")]
|
||||
InvalidDescriptorSet,
|
||||
#[error("invalid descriptor pool")]
|
||||
InvalidDescriptorPool,
|
||||
#[error("service {service} was not found in descriptor set")]
|
||||
ServiceNotFound { service: String },
|
||||
#[error("method {method} was not found in service {service}")]
|
||||
MethodNotFound { service: String, method: String },
|
||||
#[error("grpc method path is invalid for service {service} and method {method}")]
|
||||
InvalidMethodPath { service: String, method: String },
|
||||
#[error("grpc method {service}/{method} is not unary")]
|
||||
UnsupportedMethodKind { service: String, method: String },
|
||||
#[error("invalid metadata key {key}")]
|
||||
InvalidMetadataKey {
|
||||
key: String,
|
||||
#[source]
|
||||
source: InvalidMetadataKey,
|
||||
},
|
||||
#[error("invalid metadata value for key {key}")]
|
||||
InvalidMetadataValue {
|
||||
key: String,
|
||||
#[source]
|
||||
source: InvalidMetadataValue,
|
||||
},
|
||||
#[error("transport endpoint is invalid")]
|
||||
InvalidEndpoint(#[from] tonic::transport::Error),
|
||||
#[error("grpc status {code}: {message}")]
|
||||
Status { code: Code, message: String },
|
||||
}
|
||||
|
||||
impl From<Status> for GrpcAdapterError {
|
||||
fn from(value: Status) -> Self {
|
||||
Self::Status {
|
||||
code: value.code(),
|
||||
message: value.message().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-grpc"
|
||||
}
|
||||
mod client;
|
||||
mod codec;
|
||||
mod error;
|
||||
mod model;
|
||||
|
||||
pub use client::GrpcAdapter;
|
||||
pub use error::GrpcAdapterError;
|
||||
pub use model::{GrpcRequest, GrpcResponse};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test_support;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct GrpcRequest {
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub body: Value,
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GrpcResponse {
|
||||
pub status_code: u16,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub body: Value,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use tokio::net::TcpListener;
|
||||
use tonic::{Request, Response, Status, transport::Server};
|
||||
|
||||
pub mod echo {
|
||||
tonic::include_proto!("echo");
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("echo_descriptor");
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EchoServiceImpl;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl echo::echo_service_server::EchoService for EchoServiceImpl {
|
||||
async fn unary_echo(
|
||||
&self,
|
||||
request: Request<echo::EchoRequest>,
|
||||
) -> Result<Response<echo::EchoResponse>, Status> {
|
||||
Ok(Response::new(echo::EchoResponse {
|
||||
message: request.into_inner().message,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn spawn_unary_echo_server() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let incoming = tonic::transport::server::TcpIncoming::from(listener);
|
||||
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(echo::echo_service_server::EchoServiceServer::new(
|
||||
EchoServiceImpl,
|
||||
))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
pub fn descriptor_set_b64() -> String {
|
||||
STANDARD.encode(echo::FILE_DESCRIPTOR_SET)
|
||||
}
|
||||
@@ -42,6 +42,7 @@ pub struct GrpcTarget {
|
||||
pub service: String,
|
||||
pub method: String,
|
||||
pub descriptor_ref: DescriptorId,
|
||||
pub descriptor_set_b64: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -8,6 +8,8 @@ version.workspace = true
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
prost.workspace = true
|
||||
prost-reflect.workspace = true
|
||||
prost-types.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
|
||||
@@ -4,4 +4,8 @@ use thiserror::Error;
|
||||
pub enum ProtoError {
|
||||
#[error("oneof {oneof_name} must contain at least one variant")]
|
||||
EmptyOneof { oneof_name: String },
|
||||
#[error("descriptor set could not be decoded")]
|
||||
InvalidDescriptorSet,
|
||||
#[error("descriptor pool could not be built")]
|
||||
InvalidDescriptorPool,
|
||||
}
|
||||
|
||||
@@ -2,9 +2,141 @@ pub mod convert;
|
||||
pub mod errors;
|
||||
pub mod model;
|
||||
|
||||
use prost::Message;
|
||||
use prost_reflect::{
|
||||
Cardinality, DescriptorPool, EnumDescriptor, FieldDescriptor, Kind, MessageDescriptor,
|
||||
MethodDescriptor, ServiceDescriptor,
|
||||
};
|
||||
use prost_types::FileDescriptorSet;
|
||||
|
||||
pub use convert::to_schema::message_to_schema;
|
||||
pub use errors::ProtoError;
|
||||
pub use model::{
|
||||
ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoMessage,
|
||||
ProtoMethod, ProtoOneof, ProtoScalarKind, ProtoService,
|
||||
};
|
||||
|
||||
pub fn services_from_descriptor_set_bytes(bytes: &[u8]) -> Result<Vec<ProtoService>, ProtoError> {
|
||||
let descriptor_set =
|
||||
FileDescriptorSet::decode(bytes).map_err(|_| ProtoError::InvalidDescriptorSet)?;
|
||||
let pool = DescriptorPool::from_file_descriptor_set(descriptor_set)
|
||||
.map_err(|_| ProtoError::InvalidDescriptorPool)?;
|
||||
|
||||
Ok(pool.services().map(service_from_descriptor).collect())
|
||||
}
|
||||
|
||||
fn service_from_descriptor(service: ServiceDescriptor) -> ProtoService {
|
||||
ProtoService {
|
||||
package: service.parent_file().package_name().to_owned(),
|
||||
name: service.name().to_owned(),
|
||||
methods: service.methods().map(method_from_descriptor).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn method_from_descriptor(method: MethodDescriptor) -> ProtoMethod {
|
||||
ProtoMethod {
|
||||
name: method.name().to_owned(),
|
||||
input: message_from_descriptor(method.input()),
|
||||
output: message_from_descriptor(method.output()),
|
||||
client_streaming: method.is_client_streaming(),
|
||||
server_streaming: method.is_server_streaming(),
|
||||
}
|
||||
}
|
||||
|
||||
fn message_from_descriptor(message: MessageDescriptor) -> ProtoMessage {
|
||||
let fields = message
|
||||
.fields()
|
||||
.filter(|field| field.containing_oneof().is_none())
|
||||
.map(field_from_descriptor)
|
||||
.collect();
|
||||
let oneofs = message
|
||||
.oneofs()
|
||||
.map(|oneof| ProtoOneof {
|
||||
name: oneof.name().to_owned(),
|
||||
variants: oneof.fields().map(field_from_descriptor).collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
ProtoMessage {
|
||||
name: message.name().to_owned(),
|
||||
fields,
|
||||
oneofs,
|
||||
}
|
||||
}
|
||||
|
||||
fn field_from_descriptor(field: FieldDescriptor) -> ProtoField {
|
||||
let cardinality = if field.is_list() && !field.is_map() {
|
||||
ProtoFieldCardinality::Repeated
|
||||
} else {
|
||||
match field.cardinality() {
|
||||
Cardinality::Optional => ProtoFieldCardinality::Optional,
|
||||
Cardinality::Required => ProtoFieldCardinality::Required,
|
||||
Cardinality::Repeated => ProtoFieldCardinality::Repeated,
|
||||
}
|
||||
};
|
||||
|
||||
ProtoField {
|
||||
name: field.name().to_owned(),
|
||||
json_name: field.json_name().to_owned(),
|
||||
cardinality,
|
||||
field_type: field_type_from_descriptor(field),
|
||||
}
|
||||
}
|
||||
|
||||
fn field_type_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
|
||||
if field.is_map() {
|
||||
return map_field_from_descriptor(field);
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Bool => scalar_field(ProtoScalarKind::Bool),
|
||||
Kind::String => scalar_field(ProtoScalarKind::String),
|
||||
Kind::Bytes => scalar_field(ProtoScalarKind::Bytes),
|
||||
Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => scalar_field(ProtoScalarKind::Int32),
|
||||
Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => scalar_field(ProtoScalarKind::Int64),
|
||||
Kind::Uint32 | Kind::Fixed32 => scalar_field(ProtoScalarKind::Uint32),
|
||||
Kind::Uint64 | Kind::Fixed64 => scalar_field(ProtoScalarKind::Uint64),
|
||||
Kind::Float => scalar_field(ProtoScalarKind::Float),
|
||||
Kind::Double => scalar_field(ProtoScalarKind::Double),
|
||||
Kind::Message(message) => ProtoFieldType::Message {
|
||||
message: Box::new(message_from_descriptor(message)),
|
||||
},
|
||||
Kind::Enum(enumeration) => ProtoFieldType::Enum {
|
||||
enumeration: enum_from_descriptor(enumeration),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn map_field_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
|
||||
let message = match field.kind() {
|
||||
Kind::Message(message) => message,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let key_field = message.map_entry_key_field();
|
||||
let value_field = message.map_entry_value_field();
|
||||
let key = match field_type_from_descriptor(key_field) {
|
||||
ProtoFieldType::Scalar { scalar } => scalar,
|
||||
_ => ProtoScalarKind::String,
|
||||
};
|
||||
|
||||
ProtoFieldType::Map {
|
||||
map: ProtoMapField {
|
||||
key,
|
||||
value: Box::new(field_type_from_descriptor(value_field)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_field(scalar: ProtoScalarKind) -> ProtoFieldType {
|
||||
ProtoFieldType::Scalar { scalar }
|
||||
}
|
||||
|
||||
fn enum_from_descriptor(enumeration: EnumDescriptor) -> ProtoEnum {
|
||||
ProtoEnum {
|
||||
name: enumeration.name().to_owned(),
|
||||
values: enumeration
|
||||
.values()
|
||||
.map(|value| value.name().to_owned())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
axum.workspace = true
|
||||
mcpaas-adapter-grpc = { path = "../mcpaas-adapter-grpc", features = ["test-support"] }
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use mcpaas_adapter_graphql::GraphqlAdapterError;
|
||||
use mcpaas_adapter_grpc::GrpcAdapterError;
|
||||
use mcpaas_adapter_rest::RestAdapterError;
|
||||
use mcpaas_core::Protocol;
|
||||
use mcpaas_mapping::MappingError;
|
||||
@@ -14,6 +15,8 @@ pub enum RuntimeError {
|
||||
#[error(transparent)]
|
||||
GraphqlAdapter(#[from] GraphqlAdapterError),
|
||||
#[error(transparent)]
|
||||
GrpcAdapter(#[from] GrpcAdapterError),
|
||||
#[error(transparent)]
|
||||
RestAdapter(#[from] RestAdapterError),
|
||||
#[error("protocol {protocol:?} is not supported by runtime")]
|
||||
UnsupportedProtocol { protocol: Protocol },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use mcpaas_adapter_graphql::{GraphqlAdapter, GraphqlRequest};
|
||||
use mcpaas_adapter_grpc::{GrpcAdapter, GrpcRequest};
|
||||
use mcpaas_adapter_rest::{RestAdapter, RestRequest};
|
||||
use mcpaas_core::Target;
|
||||
use serde_json::{Map, Value, json};
|
||||
@@ -10,6 +11,7 @@ use crate::{AdapterResponse, PreparedRequest, RuntimeError, RuntimeOperation};
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeExecutor {
|
||||
graphql_adapter: GraphqlAdapter,
|
||||
grpc_adapter: GrpcAdapter,
|
||||
rest_adapter: RestAdapter,
|
||||
}
|
||||
|
||||
@@ -23,6 +25,7 @@ impl RuntimeExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graphql_adapter: GraphqlAdapter::new(),
|
||||
grpc_adapter: GrpcAdapter::new(),
|
||||
rest_adapter: RestAdapter::new(),
|
||||
}
|
||||
}
|
||||
@@ -38,6 +41,28 @@ impl RuntimeExecutor {
|
||||
let prepared_request = PreparedRequest::from_mapping_output(&mapped_input)?;
|
||||
|
||||
let adapter_response = match &operation.target {
|
||||
Target::Grpc(target) => {
|
||||
let request = GrpcRequest {
|
||||
headers: merge_headers(
|
||||
&BTreeMap::new(),
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
body: prepared_request
|
||||
.grpc
|
||||
.clone()
|
||||
.unwrap_or(Value::Object(Map::new())),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.grpc_adapter.execute(target, &request).await?;
|
||||
|
||||
AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body.clone(),
|
||||
data: response.body,
|
||||
}
|
||||
}
|
||||
Target::Graphql(target) => {
|
||||
let request = GraphqlRequest {
|
||||
headers: merge_headers(
|
||||
@@ -78,11 +103,6 @@ impl RuntimeExecutor {
|
||||
data: Value::Null,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(RuntimeError::UnsupportedProtocol {
|
||||
protocol: operation.protocol,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let finalized_output = finalize_output(operation, &adapter_response)?;
|
||||
@@ -106,6 +126,7 @@ impl PreparedRequest {
|
||||
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")?,
|
||||
grpc: non_empty_payload(request.get("grpc").cloned()),
|
||||
variables: non_empty_payload(request.get("variables").cloned()),
|
||||
body: request.get("body").and_then(non_empty_body).cloned(),
|
||||
})
|
||||
@@ -196,10 +217,11 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use mcpaas_adapter_grpc::test_support as grpc_test_support;
|
||||
use mcpaas_core::{
|
||||
ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlOperationType, GraphqlTarget,
|
||||
HttpMethod, Operation, OperationId, OperationStatus, Protocol, RestTarget, Samples, Target,
|
||||
ToolDescription, ToolExample,
|
||||
DescriptorId, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlOperationType,
|
||||
GraphqlTarget, GrpcTarget, HttpMethod, Operation, OperationId, OperationStatus, Protocol,
|
||||
RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
};
|
||||
use mcpaas_mapping::{MappingRule, MappingSet};
|
||||
use mcpaas_schema::{Schema, SchemaKind};
|
||||
@@ -236,6 +258,20 @@ mod tests {
|
||||
assert_eq!(output, json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_grpc_operation_end_to_end() {
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_grpc_operation(&server_addr);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "message": "hello" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "message": "hello" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_input_shape() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
@@ -523,6 +559,79 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn test_grpc_operation(server_addr: &str) -> RuntimeOperation {
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_grpc_runtime"),
|
||||
name: "echo_unary_grpc".to_owned(),
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
package: "echo".to_owned(),
|
||||
service: "EchoService".to_owned(),
|
||||
method: "UnaryEcho".to_owned(),
|
||||
descriptor_ref: DescriptorId::new("desc_echo"),
|
||||
descriptor_set_b64: grpc_test_support::descriptor_set_b64(),
|
||||
}),
|
||||
input_schema: object_schema("message", SchemaKind::String),
|
||||
output_schema: object_schema("message", SchemaKind::String),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.message".to_owned(),
|
||||
target: "$.request.grpc.message".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.message".to_owned(),
|
||||
target: "$.output.message".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: "Unary Echo gRPC".to_owned(),
|
||||
description: "Echoes a unary gRPC payload".to_owned(),
|
||||
tags: vec!["grpc".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "message": "hello" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["descriptor_set".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,
|
||||
|
||||
@@ -46,6 +46,8 @@ pub struct PreparedRequest {
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grpc: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variables: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<Value>,
|
||||
|
||||
+15
-2
@@ -216,7 +216,7 @@
|
||||
|
||||
## 7. gRPC descriptor endpoints
|
||||
|
||||
Следующие endpoints относятся к фазе `grpc-support` и не входят в `admin-api v1`:
|
||||
Следующие endpoints относятся к фазе `grpc-support` и реализованы как часть gRPC vertical slice:
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/descriptors/proto`
|
||||
|
||||
@@ -230,6 +230,15 @@
|
||||
|
||||
- загрузить `descriptor set`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"descriptor_id": "desc_01",
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/grpc/services`
|
||||
|
||||
Назначение:
|
||||
@@ -247,7 +256,9 @@
|
||||
"methods": [
|
||||
{
|
||||
"name": "CreateLead",
|
||||
"kind": "unary"
|
||||
"kind": "unary",
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -255,6 +266,8 @@
|
||||
}
|
||||
```
|
||||
|
||||
Discovery endpoint используется для выбора unary метода и для построения UI-формы входа/выхода до публикации операции.
|
||||
|
||||
## 8. Тестовый запуск
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/test-runs`
|
||||
|
||||
@@ -230,6 +230,7 @@ gRPC target:
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
- `descriptor_set_b64`
|
||||
|
||||
### Schema
|
||||
|
||||
|
||||
+7
-1
@@ -213,7 +213,8 @@
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"method": "CreateLead",
|
||||
"descriptor_ref": "desc_01hr7yn4d6g1x6vwt7h9n0e7ab"
|
||||
"descriptor_ref": "desc_01hr7yn4d6g1x6vwt7h9n0e7ab",
|
||||
"descriptor_set_b64": "<base64-encoded-descriptor-set>"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -225,6 +226,11 @@
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
- `descriptor_set_b64`
|
||||
|
||||
`descriptor_ref` остается ссылкой на загруженный descriptor artifact в storage и registry.
|
||||
|
||||
`descriptor_set_b64` - runtime-ready snapshot descriptor set, который используется unary gRPC adapter для динамического вызова метода без генерации Rust-кода.
|
||||
|
||||
## 5. `Schema`
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ classDiagram
|
||||
+service
|
||||
+method
|
||||
+descriptor_ref
|
||||
+descriptor_set_b64
|
||||
}
|
||||
|
||||
class Schema {
|
||||
|
||||
@@ -52,6 +52,7 @@ gRPC operation должна включать:
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
- `descriptor_set_b64`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
@@ -66,7 +67,7 @@ gRPC operation должна включать:
|
||||
3. Оператор выбирает конкретный unary-метод.
|
||||
4. UI показывает структуру request message и response message.
|
||||
5. При необходимости загружает примеры JSON для MCP input/output.
|
||||
6. Система строит черновую схему и стартовый mapping.
|
||||
6. Система строит черновую схему, стартовый mapping и runtime-ready snapshot descriptor set для выбранного метода.
|
||||
7. Оператор задает или уточняет входные MCP-параметры.
|
||||
8. Настраивает маппинг во входные protobuf fields.
|
||||
9. Настраивает маппинг из response fields в MCP output.
|
||||
@@ -90,6 +91,7 @@ gRPC operation должна включать:
|
||||
|
||||
- `.proto` и descriptor handling должны быть отделены от runtime-вызова;
|
||||
- protobuf discovery не должен жить внутри gRPC adapter;
|
||||
- runtime использует сохраненный `descriptor_set_b64`, а не исходный `.proto`;
|
||||
- `oneof`, `enum`, `repeated`, `map` и well-known types требуют отдельной нормализации;
|
||||
- `map` на слое нормализованной schema модели представляется как `array` объектов вида `{ key, value }`;
|
||||
- `oneof` на слое нормализованной schema модели представляется как `oneof` с вариантами-объектами, каждый из которых содержит одно допустимое поле;
|
||||
|
||||
Reference in New Issue
Block a user