diff --git a/Cargo.lock b/Cargo.lock index 29ff0d2c0..b75fc5b70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,7 @@ checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.7", ] [[package]] @@ -338,12 +338,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -1132,6 +1126,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", + "hyper 1.6.0", + "hyper-util", "itoa", "matchit", "memchr", @@ -1140,10 +1136,15 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper 1.0.2", + "tokio", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1164,6 +1165,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1754,19 +1756,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2138,6 +2150,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc16" version = "0.4.0" @@ -2241,7 +2262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.7", "curve25519-dalek-derive", "digest", "fiat-crypto 0.2.9", @@ -2325,6 +2346,16 @@ dependencies = [ "darling_macro 0.20.10", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -2353,6 +2384,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.14.4" @@ -2375,6 +2419,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -2400,6 +2455,24 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "debugid" version = "0.8.0" @@ -3530,6 +3603,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -3843,11 +3922,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -4018,6 +4111,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -4484,6 +4586,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4860,6 +4968,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "leptos" version = "0.6.11" @@ -6038,6 +6152,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -6228,7 +6348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.7", "opaque-debug", "universal-hash", ] @@ -6469,7 +6589,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls 0.23.28", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -6508,7 +6628,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -6551,6 +6671,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -6589,6 +6715,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -6627,6 +6764,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.11.0" @@ -6688,6 +6831,26 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -6891,6 +7054,50 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmcp" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12ca9067b5ebfbd5b3fcdc4acfceb81aa7d5ab2a879dff7cb75d22434276aad" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "futures", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pastey", + "pin-project-lite", + "rand 0.10.1", + "rmcp-macros", + "schemars 1.2.1", + "serde", + "serde_json", + "sse-stream", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "rmcp-macros" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7caa6743cc0888e433105fe1bc551a7f607940b126a37bc97b478e86064627eb" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "rowan" version = "0.15.13" @@ -7007,7 +7214,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7231,6 +7438,56 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive 1.2.1", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -7445,6 +7702,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_fmt" version = "1.0.3" @@ -7694,7 +7962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.7", "digest", ] @@ -7705,7 +7973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.7", "digest", ] @@ -7838,6 +8106,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smithy-mcp-runtime" +version = "0.1.0" +source = "git+https://github.com/juspay/smithy-mcp-generator.git?rev=6ec7445755c1410c052947f00eef1dd65011aadc#6ec7445755c1410c052947f00eef1dd65011aadc" +dependencies = [ + "async-trait", + "aws-smithy-types", + "rmcp", + "schemars 0.8.22", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "socket2" version = "0.4.9" @@ -7908,6 +8190,19 @@ dependencies = [ "der", ] +[[package]] +name = "sse-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72" +dependencies = [ + "bytes", + "futures-util", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7924,7 +8219,6 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.52.0", "windows-sys 0.59.0", ] @@ -8097,6 +8391,46 @@ dependencies = [ name = "superposition_macros" version = "0.106.2" +[[package]] +name = "superposition_mcp" +version = "0.106.2" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "smithy-mcp-runtime", + "superposition_sdk", + "tokio", +] + +[[package]] +name = "superposition_mcp_server" +version = "0.106.2" +dependencies = [ + "anyhow", + "async-trait", + "aws-smithy-http-client", + "aws-smithy-runtime-api", + "aws-smithy-types", + "axum", + "base64 0.21.2", + "clap 4.3.4", + "http 1.1.0", + "rmcp", + "secrecy", + "serde", + "serde_json", + "smithy-mcp-runtime", + "superposition_mcp", + "superposition_sdk", + "thiserror 1.0.58", + "tokio", + "tower 0.5.2", + "tracing", + "tracing-subscriber", + "wiremock", +] + [[package]] name = "superposition_provider" version = "0.106.2" @@ -9043,16 +9377,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -9205,6 +9538,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -9872,6 +10206,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -9967,6 +10319,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.0" @@ -9990,6 +10364,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.9.1", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.17", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -10518,6 +10904,56 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wiremock" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.22.1", + "deadpool", + "futures", + "http 1.1.0", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -10527,6 +10963,74 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.17", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 48423bf56..eab4e6eb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ members = [ "crates/superposition", "crates/superposition_types", "crates/superposition_macros", + "crates/superposition_mcp", + "crates/superposition_mcp_server", "crates/superposition_derives", "crates/superposition_core", "crates/superposition_provider", diff --git a/crates/superposition_mcp/CHANGELOG.md b/crates/superposition_mcp/CHANGELOG.md new file mode 100644 index 000000000..89ded0218 --- /dev/null +++ b/crates/superposition_mcp/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +This file is hand-maintained alongside the otherwise auto-generated `superposition_mcp` crate. + +## Unreleased diff --git a/crates/superposition_mcp/Cargo.toml b/crates/superposition_mcp/Cargo.toml new file mode 100644 index 000000000..63f358d2f --- /dev/null +++ b/crates/superposition_mcp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "superposition_mcp" +version.workspace = true +edition = "2021" +license = { workspace = true } +homepage = { workspace = true } +readme = "README.md" + +[dependencies] +smithy-mcp-runtime = { git = "https://github.com/juspay/smithy-mcp-generator.git", rev = "6ec7445755c1410c052947f00eef1dd65011aadc", features = ["stdio", "http"] } +superposition_sdk = { path = "../superposition_sdk" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +async-trait = "0.1" +tokio = { version = "1", features = ["full"] } diff --git a/crates/superposition_mcp/README.md b/crates/superposition_mcp/README.md new file mode 100644 index 000000000..16cfb95fa --- /dev/null +++ b/crates/superposition_mcp/README.md @@ -0,0 +1,9 @@ +# superposition_mcp + +Generated Rust crate exposing the `io.superposition#Superposition` smithy service as an MCP (Model Context Protocol) server. + +**Do not edit files in this crate by hand** — they are regenerated by `make smithy-clients` from `smithy/models/*.smithy`. The exceptions are `README.md` and `CHANGELOG.md`, which are hand-preserved and restored after regeneration. + +The deployable binary that wraps this crate lives in `../superposition_mcp_server/`. + +See `docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md` for design context. diff --git a/crates/superposition_mcp/src/conversions.rs b/crates/superposition_mcp/src/conversions.rs new file mode 100644 index 000000000..7cfe57c58 --- /dev/null +++ b/crates/superposition_mcp/src/conversions.rs @@ -0,0 +1,3280 @@ +//! Generated bridges between MCP-side serializable types and smithy-rs SDK types. +//! +//! This file is regenerated by smithy-mcp-codegen — do not edit by hand. +#![allow(clippy::useless_conversion)] +#![allow(unused_imports)] + +use crate::types::*; + +impl From for superposition_sdk::operation::add_members_to_group::builders::AddMembersToGroupInputBuilder { + fn from(input: crate::types::ModifyMembersToGroupRequest) -> Self { + let mut builder = superposition_sdk::operation::add_members_to_group::AddMembersToGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_member_experiment_ids(Some(input.member_experiment_ids)); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::add_members_to_group::AddMembersToGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::applicable_variants::builders::ApplicableVariantsInputBuilder { + fn from(input: crate::types::ApplicableVariantsInput) -> Self { + let mut builder = superposition_sdk::operation::applicable_variants::ApplicableVariantsInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_identifier(Some(input.identifier)); + builder = builder.set_prefix(input.prefix); + builder + } +} + +impl From for crate::types::ApplicableVariantsOutput { + fn from(sdk: superposition_sdk::operation::applicable_variants::ApplicableVariantsOutput) -> Self { + Self { + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::bulk_operation::builders::BulkOperationInputBuilder { + fn from(input: crate::types::BulkOperationInput) -> Self { + let mut builder = superposition_sdk::operation::bulk_operation::BulkOperationInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_config_tags(input.config_tags); + builder = builder.set_operations(Some(input.operations.into_iter().map(Into::into).collect())); + builder + } +} + +impl From for crate::types::BulkOperationOutput { + fn from(sdk: superposition_sdk::operation::bulk_operation::BulkOperationOutput) -> Self { + Self { + output: sdk.output.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::conclude_experiment::builders::ConcludeExperimentInputBuilder { + fn from(input: crate::types::ConcludeExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::conclude_experiment::ConcludeExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_chosen_variant(Some(input.chosen_variant)); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::conclude_experiment::ConcludeExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::create_context::builders::CreateContextInputBuilder { + fn from(input: crate::types::CreateContextInput) -> Self { + let mut builder = superposition_sdk::operation::create_context::CreateContextInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_config_tags(input.config_tags); + builder = builder.set_request(Some(input.request.into())); + builder + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::operation::create_context::CreateContextOutput) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::create_default_config::builders::CreateDefaultConfigInputBuilder { + fn from(input: crate::types::CreateDefaultConfigInput) -> Self { + let mut builder = superposition_sdk::operation::create_default_config::CreateDefaultConfigInput::builder(); + builder = builder.set_key(Some(input.key)); + builder = builder.set_value(Some(smithy_mcp_runtime::value_to_document(input.value))); + builder = builder.set_schema(Some(input.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_value_validation_function_name(input.value_validation_function_name); + builder = builder.set_value_compute_function_name(input.value_compute_function_name); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder + } +} + +impl From for crate::types::DefaultConfigResponse { + fn from(sdk: superposition_sdk::operation::create_default_config::CreateDefaultConfigOutput) -> Self { + Self { + key: sdk.key, + value: smithy_mcp_runtime::document_to_value(sdk.value), + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + value_validation_function_name: sdk.value_validation_function_name, + value_compute_function_name: sdk.value_compute_function_name, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::create_dimension::builders::CreateDimensionInputBuilder { + fn from(input: crate::types::CreateDimensionInput) -> Self { + let mut builder = superposition_sdk::operation::create_dimension::CreateDimensionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_dimension(Some(input.dimension)); + builder = builder.set_position(Some(input.position)); + builder = builder.set_schema(Some(input.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_value_validation_function_name(input.value_validation_function_name); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_dimension_type(input.dimension_type.map(Into::into)); + builder = builder.set_value_compute_function_name(input.value_compute_function_name); + builder + } +} + +impl From for crate::types::DimensionResponse { + fn from(sdk: superposition_sdk::operation::create_dimension::CreateDimensionOutput) -> Self { + Self { + dimension: sdk.dimension, + position: sdk.position, + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + value_validation_function_name: sdk.value_validation_function_name, + description: sdk.description, + change_reason: sdk.change_reason, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + dependency_graph: sdk.dependency_graph, + dimension_type: sdk.dimension_type.into(), + value_compute_function_name: sdk.value_compute_function_name, + mandatory: sdk.mandatory, + } + } +} + +impl From for superposition_sdk::operation::create_experiment::builders::CreateExperimentInputBuilder { + fn from(input: crate::types::CreateExperimentRequest) -> Self { + let mut builder = superposition_sdk::operation::create_experiment::CreateExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_experiment_type(input.experiment_type.map(Into::into)); + builder = builder.set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_variants(Some(input.variants.into_iter().map(Into::into).collect())); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_metrics(input.metrics.map(smithy_mcp_runtime::value_to_document)); + builder = builder.set_experiment_group_id(input.experiment_group_id); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::create_experiment::CreateExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::create_experiment_group::builders::CreateExperimentGroupInputBuilder { + fn from(input: crate::types::CreateExperimentGroupRequest) -> Self { + let mut builder = superposition_sdk::operation::create_experiment_group::CreateExperimentGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_traffic_percentage(Some(input.traffic_percentage)); + builder = builder.set_member_experiment_ids(input.member_experiment_ids); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::create_experiment_group::CreateExperimentGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::create_function::builders::CreateFunctionInputBuilder { + fn from(input: crate::types::CreateFunctionRequest) -> Self { + let mut builder = superposition_sdk::operation::create_function::CreateFunctionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_function(Some(input.function)); + builder = builder.set_runtime_version(Some(input.runtime_version.into())); + builder = builder.set_function_type(Some(input.function_type.into())); + builder + } +} + +impl From for crate::types::FunctionResponse { + fn from(sdk: superposition_sdk::operation::create_function::CreateFunctionOutput) -> Self { + Self { + function_name: sdk.function_name, + published_code: sdk.published_code, + draft_code: sdk.draft_code, + published_runtime_version: sdk.published_runtime_version.map(Into::into), + draft_runtime_version: sdk.draft_runtime_version.into(), + published_at: sdk.published_at.map(smithy_mcp_runtime::datetime_to_string), + draft_edited_at: smithy_mcp_runtime::datetime_to_string(sdk.draft_edited_at), + published_by: sdk.published_by, + draft_edited_by: sdk.draft_edited_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + change_reason: sdk.change_reason, + description: sdk.description, + function_type: sdk.function_type.into(), + } + } +} + +impl From for superposition_sdk::operation::create_organisation::builders::CreateOrganisationInputBuilder { + fn from(input: crate::types::CreateOrganisationRequest) -> Self { + let mut builder = superposition_sdk::operation::create_organisation::CreateOrganisationInput::builder(); + builder = builder.set_country_code(input.country_code); + builder = builder.set_contact_email(input.contact_email); + builder = builder.set_contact_phone(input.contact_phone); + builder = builder.set_admin_email(Some(input.admin_email)); + builder = builder.set_sector(input.sector); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::OrganisationResponse { + fn from(sdk: superposition_sdk::operation::create_organisation::CreateOrganisationOutput) -> Self { + Self { + id: sdk.id, + name: sdk.name, + country_code: sdk.country_code, + contact_email: sdk.contact_email, + contact_phone: sdk.contact_phone, + created_by: sdk.created_by, + admin_email: sdk.admin_email, + status: sdk.status.into(), + sector: sdk.sector, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + updated_at: smithy_mcp_runtime::datetime_to_string(sdk.updated_at), + updated_by: sdk.updated_by, + } + } +} + +impl From for superposition_sdk::operation::create_secret::builders::CreateSecretInputBuilder { + fn from(input: crate::types::CreateSecretInput) -> Self { + let mut builder = superposition_sdk::operation::create_secret::CreateSecretInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_value(Some(input.value)); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::SecretResponse { + fn from(sdk: superposition_sdk::operation::create_secret::CreateSecretOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::create_type_templates::builders::CreateTypeTemplatesInputBuilder { + fn from(input: crate::types::CreateTypeTemplatesRequest) -> Self { + let mut builder = superposition_sdk::operation::create_type_templates::CreateTypeTemplatesInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_type_name(Some(input.type_name)); + builder = builder.set_type_schema(Some(input.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::TypeTemplatesResponse { + fn from(sdk: superposition_sdk::operation::create_type_templates::CreateTypeTemplatesOutput) -> Self { + Self { + type_name: sdk.type_name, + type_schema: sdk.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::create_variable::builders::CreateVariableInputBuilder { + fn from(input: crate::types::CreateVariableInput) -> Self { + let mut builder = superposition_sdk::operation::create_variable::CreateVariableInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_value(Some(input.value)); + builder = builder.set_description(Some(input.description)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::VariableResponse { + fn from(sdk: superposition_sdk::operation::create_variable::CreateVariableOutput) -> Self { + Self { + name: sdk.name, + value: sdk.value, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::create_webhook::builders::CreateWebhookInputBuilder { + fn from(input: crate::types::CreateWebhookInput) -> Self { + let mut builder = superposition_sdk::operation::create_webhook::CreateWebhookInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_description(Some(input.description)); + builder = builder.set_enabled(Some(input.enabled)); + builder = builder.set_url(Some(input.url)); + builder = builder.set_method(Some(input.method.into())); + builder = builder.set_version(input.version.map(Into::into)); + builder = builder.set_custom_headers(input.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_events(Some(input.events)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::WebhookResponse { + fn from(sdk: superposition_sdk::operation::create_webhook::CreateWebhookOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + enabled: sdk.enabled, + url: sdk.url, + method: sdk.method.into(), + version: sdk.version.into(), + custom_headers: sdk.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect()), + events: sdk.events, + max_retries: sdk.max_retries, + last_triggered_at: sdk.last_triggered_at.map(smithy_mcp_runtime::datetime_to_string), + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::create_workspace::builders::CreateWorkspaceInputBuilder { + fn from(input: crate::types::CreateWorkspaceRequest) -> Self { + let mut builder = superposition_sdk::operation::create_workspace::CreateWorkspaceInput::builder(); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_workspace_admin_email(Some(input.workspace_admin_email)); + builder = builder.set_workspace_name(Some(input.workspace_name)); + builder = builder.set_workspace_status(input.workspace_status.map(Into::into)); + builder = builder.set_metrics(input.metrics.map(smithy_mcp_runtime::value_to_document)); + builder = builder.set_allow_experiment_self_approval(input.allow_experiment_self_approval); + builder = builder.set_auto_populate_control(input.auto_populate_control); + builder = builder.set_enable_context_validation(input.enable_context_validation); + builder = builder.set_enable_change_reason_validation(input.enable_change_reason_validation); + builder + } +} + +impl From for crate::types::WorkspaceResponse { + fn from(sdk: superposition_sdk::operation::create_workspace::CreateWorkspaceOutput) -> Self { + Self { + workspace_name: sdk.workspace_name, + organisation_id: sdk.organisation_id, + organisation_name: sdk.organisation_name, + workspace_schema_name: sdk.workspace_schema_name, + workspace_status: sdk.workspace_status.into(), + workspace_admin_email: sdk.workspace_admin_email, + config_version: sdk.config_version, + created_by: sdk.created_by, + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + mandatory_dimensions: sdk.mandatory_dimensions, + metrics: smithy_mcp_runtime::document_to_value(sdk.metrics), + allow_experiment_self_approval: sdk.allow_experiment_self_approval, + auto_populate_control: sdk.auto_populate_control, + enable_context_validation: sdk.enable_context_validation, + enable_change_reason_validation: sdk.enable_change_reason_validation, + } + } +} + +impl From for superposition_sdk::operation::delete_context::builders::DeleteContextInputBuilder { + fn from(input: crate::types::DeleteContextInput) -> Self { + let mut builder = superposition_sdk::operation::delete_context::DeleteContextInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_config_tags(input.config_tags); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::delete_context::DeleteContextOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::delete_default_config::builders::DeleteDefaultConfigInputBuilder { + fn from(input: crate::types::DeleteDefaultConfigInput) -> Self { + let mut builder = superposition_sdk::operation::delete_default_config::DeleteDefaultConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_key(Some(input.key)); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::delete_default_config::DeleteDefaultConfigOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::delete_dimension::builders::DeleteDimensionInputBuilder { + fn from(input: crate::types::DeleteDimensionInput) -> Self { + let mut builder = superposition_sdk::operation::delete_dimension::DeleteDimensionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_dimension(Some(input.dimension)); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::delete_dimension::DeleteDimensionOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::delete_experiment_group::builders::DeleteExperimentGroupInputBuilder { + fn from(input: crate::types::DeleteExperimentGroupInput) -> Self { + let mut builder = superposition_sdk::operation::delete_experiment_group::DeleteExperimentGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::delete_experiment_group::DeleteExperimentGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::delete_function::builders::DeleteFunctionInputBuilder { + fn from(input: crate::types::DeleteFunctionInput) -> Self { + let mut builder = superposition_sdk::operation::delete_function::DeleteFunctionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::delete_function::DeleteFunctionOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::delete_secret::builders::DeleteSecretInputBuilder { + fn from(input: crate::types::DeleteSecretInput) -> Self { + let mut builder = superposition_sdk::operation::delete_secret::DeleteSecretInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::SecretResponse { + fn from(sdk: superposition_sdk::operation::delete_secret::DeleteSecretOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::delete_type_templates::builders::DeleteTypeTemplatesInputBuilder { + fn from(input: crate::types::DeleteTypeTemplatesInput) -> Self { + let mut builder = superposition_sdk::operation::delete_type_templates::DeleteTypeTemplatesInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_type_name(Some(input.type_name)); + builder + } +} + +impl From for crate::types::TypeTemplatesResponse { + fn from(sdk: superposition_sdk::operation::delete_type_templates::DeleteTypeTemplatesOutput) -> Self { + Self { + type_name: sdk.type_name, + type_schema: sdk.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::delete_variable::builders::DeleteVariableInputBuilder { + fn from(input: crate::types::DeleteVariableInput) -> Self { + let mut builder = superposition_sdk::operation::delete_variable::DeleteVariableInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::VariableResponse { + fn from(sdk: superposition_sdk::operation::delete_variable::DeleteVariableOutput) -> Self { + Self { + name: sdk.name, + value: sdk.value, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::delete_webhook::builders::DeleteWebhookInputBuilder { + fn from(input: crate::types::DeleteWebhookInput) -> Self { + let mut builder = superposition_sdk::operation::delete_webhook::DeleteWebhookInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::delete_webhook::DeleteWebhookOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::discard_experiment::builders::DiscardExperimentInputBuilder { + fn from(input: crate::types::DiscardExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::discard_experiment::DiscardExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::discard_experiment::DiscardExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::get_config::builders::GetConfigInputBuilder { + fn from(input: crate::types::GetConfigInput) -> Self { + let mut builder = superposition_sdk::operation::get_config::GetConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_prefix(input.prefix); + builder = builder.set_version(input.version); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder + } +} + +impl From for crate::types::GetConfigOutput { + fn from(sdk: superposition_sdk::operation::get_config::GetConfigOutput) -> Self { + Self { + contexts: sdk.contexts.into_iter().map(Into::into).collect(), + overrides: sdk.overrides.into_iter().map(|(k, v)| (k, v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect())).collect(), + default_configs: sdk.default_configs.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + dimensions: sdk.dimensions.into_iter().map(|(k, v)| (k, v.into())).collect(), + version: sdk.version, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + } + } +} + +impl From for superposition_sdk::operation::get_config_json::builders::GetConfigJsonInputBuilder { + fn from(input: crate::types::GetConfigJsonInput) -> Self { + let mut builder = superposition_sdk::operation::get_config_json::GetConfigJsonInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder + } +} + +impl From for crate::types::GetConfigJsonOutput { + fn from(sdk: superposition_sdk::operation::get_config_json::GetConfigJsonOutput) -> Self { + Self { + json_config: sdk.json_config, + last_modified: sdk.last_modified.map(smithy_mcp_runtime::datetime_to_string), + } + } +} + +impl From for superposition_sdk::operation::get_config_toml::builders::GetConfigTomlInputBuilder { + fn from(input: crate::types::GetConfigTomlInput) -> Self { + let mut builder = superposition_sdk::operation::get_config_toml::GetConfigTomlInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder + } +} + +impl From for crate::types::GetConfigTomlOutput { + fn from(sdk: superposition_sdk::operation::get_config_toml::GetConfigTomlOutput) -> Self { + Self { + toml_config: sdk.toml_config, + last_modified: sdk.last_modified.map(smithy_mcp_runtime::datetime_to_string), + } + } +} + +impl From for superposition_sdk::operation::get_context::builders::GetContextInputBuilder { + fn from(input: crate::types::GetContextInput) -> Self { + let mut builder = superposition_sdk::operation::get_context::GetContextInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::operation::get_context::GetContextOutput) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::get_context_from_condition::builders::GetContextFromConditionInputBuilder { + fn from(input: crate::types::GetContextFromConditionInput) -> Self { + let mut builder = superposition_sdk::operation::get_context_from_condition::GetContextFromConditionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_context(input.context.map(smithy_mcp_runtime::value_to_document)); + builder + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::operation::get_context_from_condition::GetContextFromConditionOutput) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::get_default_config::builders::GetDefaultConfigInputBuilder { + fn from(input: crate::types::GetDefaultConfigInput) -> Self { + let mut builder = superposition_sdk::operation::get_default_config::GetDefaultConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_key(Some(input.key)); + builder + } +} + +impl From for crate::types::DefaultConfigResponse { + fn from(sdk: superposition_sdk::operation::get_default_config::GetDefaultConfigOutput) -> Self { + Self { + key: sdk.key, + value: smithy_mcp_runtime::document_to_value(sdk.value), + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + value_validation_function_name: sdk.value_validation_function_name, + value_compute_function_name: sdk.value_compute_function_name, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::get_dimension::builders::GetDimensionInputBuilder { + fn from(input: crate::types::GetDimensionInput) -> Self { + let mut builder = superposition_sdk::operation::get_dimension::GetDimensionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_dimension(Some(input.dimension)); + builder + } +} + +impl From for crate::types::DimensionResponse { + fn from(sdk: superposition_sdk::operation::get_dimension::GetDimensionOutput) -> Self { + Self { + dimension: sdk.dimension, + position: sdk.position, + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + value_validation_function_name: sdk.value_validation_function_name, + description: sdk.description, + change_reason: sdk.change_reason, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + dependency_graph: sdk.dependency_graph, + dimension_type: sdk.dimension_type.into(), + value_compute_function_name: sdk.value_compute_function_name, + mandatory: sdk.mandatory, + } + } +} + +impl From for superposition_sdk::operation::get_experiment::builders::GetExperimentInputBuilder { + fn from(input: crate::types::GetExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::get_experiment::GetExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::get_experiment::GetExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::get_experiment_config::builders::GetExperimentConfigInputBuilder { + fn from(input: crate::types::GetExperimentConfigInput) -> Self { + let mut builder = superposition_sdk::operation::get_experiment_config::GetExperimentConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_prefix(input.prefix); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_dimension_match_strategy(input.dimension_match_strategy.map(Into::into)); + builder + } +} + +impl From for crate::types::GetExperimentConfigOutput { + fn from(sdk: superposition_sdk::operation::get_experiment_config::GetExperimentConfigOutput) -> Self { + Self { + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + experiments: sdk.experiments.into_iter().map(Into::into).collect(), + experiment_groups: sdk.experiment_groups.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::get_experiment_group::builders::GetExperimentGroupInputBuilder { + fn from(input: crate::types::GetExperimentGroupInput) -> Self { + let mut builder = superposition_sdk::operation::get_experiment_group::GetExperimentGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::get_experiment_group::GetExperimentGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::get_function::builders::GetFunctionInputBuilder { + fn from(input: crate::types::GetFunctionInput) -> Self { + let mut builder = superposition_sdk::operation::get_function::GetFunctionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder + } +} + +impl From for crate::types::FunctionResponse { + fn from(sdk: superposition_sdk::operation::get_function::GetFunctionOutput) -> Self { + Self { + function_name: sdk.function_name, + published_code: sdk.published_code, + draft_code: sdk.draft_code, + published_runtime_version: sdk.published_runtime_version.map(Into::into), + draft_runtime_version: sdk.draft_runtime_version.into(), + published_at: sdk.published_at.map(smithy_mcp_runtime::datetime_to_string), + draft_edited_at: smithy_mcp_runtime::datetime_to_string(sdk.draft_edited_at), + published_by: sdk.published_by, + draft_edited_by: sdk.draft_edited_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + change_reason: sdk.change_reason, + description: sdk.description, + function_type: sdk.function_type.into(), + } + } +} + +impl From for superposition_sdk::operation::get_organisation::builders::GetOrganisationInputBuilder { + fn from(input: crate::types::GetOrganisationInput) -> Self { + let mut builder = superposition_sdk::operation::get_organisation::GetOrganisationInput::builder(); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::OrganisationResponse { + fn from(sdk: superposition_sdk::operation::get_organisation::GetOrganisationOutput) -> Self { + Self { + id: sdk.id, + name: sdk.name, + country_code: sdk.country_code, + contact_email: sdk.contact_email, + contact_phone: sdk.contact_phone, + created_by: sdk.created_by, + admin_email: sdk.admin_email, + status: sdk.status.into(), + sector: sdk.sector, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + updated_at: smithy_mcp_runtime::datetime_to_string(sdk.updated_at), + updated_by: sdk.updated_by, + } + } +} + +impl From for superposition_sdk::operation::get_resolved_config::builders::GetResolvedConfigInputBuilder { + fn from(input: crate::types::GetResolvedConfigInput) -> Self { + let mut builder = superposition_sdk::operation::get_resolved_config::GetResolvedConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_prefix(input.prefix); + builder = builder.set_version(input.version); + builder = builder.set_show_reasoning(input.show_reasoning); + builder = builder.set_merge_strategy(input.merge_strategy.map(Into::into)); + builder = builder.set_context_id(input.context_id); + builder = builder.set_resolve_remote(input.resolve_remote); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder + } +} + +impl From for crate::types::GetResolvedConfigOutput { + fn from(sdk: superposition_sdk::operation::get_resolved_config::GetResolvedConfigOutput) -> Self { + Self { + config: smithy_mcp_runtime::document_to_value(sdk.config), + version: sdk.version, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + audit_id: sdk.audit_id, + } + } +} + +impl From for superposition_sdk::operation::get_resolved_config_with_identifier::builders::GetResolvedConfigWithIdentifierInputBuilder { + fn from(input: crate::types::GetResolvedConfigWithIdentifierInput) -> Self { + let mut builder = superposition_sdk::operation::get_resolved_config_with_identifier::GetResolvedConfigWithIdentifierInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_prefix(input.prefix); + builder = builder.set_version(input.version); + builder = builder.set_show_reasoning(input.show_reasoning); + builder = builder.set_merge_strategy(input.merge_strategy.map(Into::into)); + builder = builder.set_context_id(input.context_id); + builder = builder.set_resolve_remote(input.resolve_remote); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_identifier(input.identifier); + builder + } +} + +impl From for crate::types::GetResolvedConfigWithIdentifierOutput { + fn from(sdk: superposition_sdk::operation::get_resolved_config_with_identifier::GetResolvedConfigWithIdentifierOutput) -> Self { + Self { + config: smithy_mcp_runtime::document_to_value(sdk.config), + version: sdk.version, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + audit_id: sdk.audit_id, + } + } +} + +impl From for superposition_sdk::operation::get_secret::builders::GetSecretInputBuilder { + fn from(input: crate::types::GetSecretInput) -> Self { + let mut builder = superposition_sdk::operation::get_secret::GetSecretInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::SecretResponse { + fn from(sdk: superposition_sdk::operation::get_secret::GetSecretOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::get_type_template::builders::GetTypeTemplateInputBuilder { + fn from(input: crate::types::GetTypeTemplateInput) -> Self { + let mut builder = superposition_sdk::operation::get_type_template::GetTypeTemplateInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_type_name(Some(input.type_name)); + builder + } +} + +impl From for crate::types::TypeTemplatesResponse { + fn from(sdk: superposition_sdk::operation::get_type_template::GetTypeTemplateOutput) -> Self { + Self { + type_name: sdk.type_name, + type_schema: sdk.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::get_type_templates_list::builders::GetTypeTemplatesListInputBuilder { + fn from(input: crate::types::GetTypeTemplatesListInput) -> Self { + let mut builder = superposition_sdk::operation::get_type_templates_list::GetTypeTemplatesListInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder + } +} + +impl From for crate::types::GetTypeTemplatesListOutput { + fn from(sdk: superposition_sdk::operation::get_type_templates_list::GetTypeTemplatesListOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::get_variable::builders::GetVariableInputBuilder { + fn from(input: crate::types::GetVariableInput) -> Self { + let mut builder = superposition_sdk::operation::get_variable::GetVariableInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::VariableResponse { + fn from(sdk: superposition_sdk::operation::get_variable::GetVariableOutput) -> Self { + Self { + name: sdk.name, + value: sdk.value, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::get_version::builders::GetVersionInputBuilder { + fn from(input: crate::types::GetVersionInput) -> Self { + let mut builder = superposition_sdk::operation::get_version::GetVersionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder + } +} + +impl From for crate::types::GetVersionResponse { + fn from(sdk: superposition_sdk::operation::get_version::GetVersionOutput) -> Self { + Self { + id: sdk.id, + config: sdk.config.into(), + config_hash: sdk.config_hash, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + description: sdk.description, + tags: sdk.tags, + } + } +} + +impl From for superposition_sdk::operation::get_webhook::builders::GetWebhookInputBuilder { + fn from(input: crate::types::GetWebhookInput) -> Self { + let mut builder = superposition_sdk::operation::get_webhook::GetWebhookInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder + } +} + +impl From for crate::types::WebhookResponse { + fn from(sdk: superposition_sdk::operation::get_webhook::GetWebhookOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + enabled: sdk.enabled, + url: sdk.url, + method: sdk.method.into(), + version: sdk.version.into(), + custom_headers: sdk.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect()), + events: sdk.events, + max_retries: sdk.max_retries, + last_triggered_at: sdk.last_triggered_at.map(smithy_mcp_runtime::datetime_to_string), + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::get_webhook_by_event::builders::GetWebhookByEventInputBuilder { + fn from(input: crate::types::GetWebhookByEventInput) -> Self { + let mut builder = superposition_sdk::operation::get_webhook_by_event::GetWebhookByEventInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_event(Some(input.event)); + builder + } +} + +impl From for crate::types::WebhookResponse { + fn from(sdk: superposition_sdk::operation::get_webhook_by_event::GetWebhookByEventOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + enabled: sdk.enabled, + url: sdk.url, + method: sdk.method.into(), + version: sdk.version.into(), + custom_headers: sdk.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect()), + events: sdk.events, + max_retries: sdk.max_retries, + last_triggered_at: sdk.last_triggered_at.map(smithy_mcp_runtime::datetime_to_string), + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::get_workspace::builders::GetWorkspaceInputBuilder { + fn from(input: crate::types::GetWorkspaceInput) -> Self { + let mut builder = superposition_sdk::operation::get_workspace::GetWorkspaceInput::builder(); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_workspace_name(Some(input.workspace_name)); + builder + } +} + +impl From for crate::types::WorkspaceResponse { + fn from(sdk: superposition_sdk::operation::get_workspace::GetWorkspaceOutput) -> Self { + Self { + workspace_name: sdk.workspace_name, + organisation_id: sdk.organisation_id, + organisation_name: sdk.organisation_name, + workspace_schema_name: sdk.workspace_schema_name, + workspace_status: sdk.workspace_status.into(), + workspace_admin_email: sdk.workspace_admin_email, + config_version: sdk.config_version, + created_by: sdk.created_by, + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + mandatory_dimensions: sdk.mandatory_dimensions, + metrics: smithy_mcp_runtime::document_to_value(sdk.metrics), + allow_experiment_self_approval: sdk.allow_experiment_self_approval, + auto_populate_control: sdk.auto_populate_control, + enable_context_validation: sdk.enable_context_validation, + enable_change_reason_validation: sdk.enable_change_reason_validation, + } + } +} + +impl From for superposition_sdk::operation::list_audit_logs::builders::ListAuditLogsInputBuilder { + fn from(input: crate::types::ListAuditLogsInput) -> Self { + let mut builder = superposition_sdk::operation::list_audit_logs::ListAuditLogsInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_from_date(input.from_date.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_to_date(input.to_date.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_tables(input.tables); + builder = builder.set_action(input.action.map(|v| v.into_iter().map(Into::into).collect())); + builder = builder.set_username(input.username); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder + } +} + +impl From for crate::types::ListAuditLogsOutput { + fn from(sdk: superposition_sdk::operation::list_audit_logs::ListAuditLogsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_contexts::builders::ListContextsInputBuilder { + fn from(input: crate::types::ListContextsInput) -> Self { + let mut builder = superposition_sdk::operation::list_contexts::ListContextsInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_prefix(input.prefix); + builder = builder.set_sort_on(input.sort_on.map(Into::into)); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder = builder.set_created_by(input.created_by); + builder = builder.set_last_modified_by(input.last_modified_by); + builder = builder.set_plaintext(input.plaintext); + builder = builder.set_dimension_match_strategy(input.dimension_match_strategy.map(Into::into)); + builder + } +} + +impl From for crate::types::ListContextsOutput { + fn from(sdk: superposition_sdk::operation::list_contexts::ListContextsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_default_configs::builders::ListDefaultConfigsInputBuilder { + fn from(input: crate::types::ListDefaultConfigsInput) -> Self { + let mut builder = superposition_sdk::operation::list_default_configs::ListDefaultConfigsInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_name(input.name); + builder + } +} + +impl From for crate::types::ListDefaultConfigsOutput { + fn from(sdk: superposition_sdk::operation::list_default_configs::ListDefaultConfigsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_dimensions::builders::ListDimensionsInputBuilder { + fn from(input: crate::types::ListDimensionsInput) -> Self { + let mut builder = superposition_sdk::operation::list_dimensions::ListDimensionsInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder + } +} + +impl From for crate::types::ListDimensionsOutput { + fn from(sdk: superposition_sdk::operation::list_dimensions::ListDimensionsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_experiment::builders::ListExperimentInputBuilder { + fn from(input: crate::types::ListExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::list_experiment::ListExperimentInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_status(input.status.map(|v| v.into_iter().map(Into::into).collect())); + builder = builder.set_from_date(input.from_date.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_to_date(input.to_date.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_experiment_name(input.experiment_name); + builder = builder.set_experiment_ids(input.experiment_ids); + builder = builder.set_experiment_group_ids(input.experiment_group_ids); + builder = builder.set_created_by(input.created_by); + builder = builder.set_sort_on(input.sort_on.map(Into::into)); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder = builder.set_global_experiments_only(input.global_experiments_only); + builder = builder.set_dimension_match_strategy(input.dimension_match_strategy.map(Into::into)); + builder = builder.set_prefix(input.prefix); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder + } +} + +impl From for crate::types::ListExperimentOutput { + fn from(sdk: superposition_sdk::operation::list_experiment::ListExperimentOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + } + } +} + +impl From for superposition_sdk::operation::list_experiment_groups::builders::ListExperimentGroupsInputBuilder { + fn from(input: crate::types::ListExperimentGroupsInput) -> Self { + let mut builder = superposition_sdk::operation::list_experiment_groups::ListExperimentGroupsInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_if_modified_since(input.if_modified_since.map(|s| smithy_mcp_runtime::string_to_datetime(&s))); + builder = builder.set_name(input.name); + builder = builder.set_created_by(input.created_by); + builder = builder.set_last_modified_by(input.last_modified_by); + builder = builder.set_sort_on(input.sort_on.map(Into::into)); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder = builder.set_group_type(input.group_type.map(|v| v.into_iter().map(Into::into).collect())); + builder = builder.set_dimension_match_strategy(input.dimension_match_strategy.map(Into::into)); + builder = builder.set_context(input.context.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder + } +} + +impl From for crate::types::ListExperimentGroupsOutput { + fn from(sdk: superposition_sdk::operation::list_experiment_groups::ListExperimentGroupsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + } + } +} + +impl From for superposition_sdk::operation::list_function::builders::ListFunctionInputBuilder { + fn from(input: crate::types::ListFunctionInput) -> Self { + let mut builder = superposition_sdk::operation::list_function::ListFunctionInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_type(input.function_type.map(|v| v.into_iter().map(Into::into).collect())); + builder + } +} + +impl From for crate::types::ListFunctionOutput { + fn from(sdk: superposition_sdk::operation::list_function::ListFunctionOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_organisation::builders::ListOrganisationInputBuilder { + fn from(input: crate::types::ListOrganisationInput) -> Self { + let mut builder = superposition_sdk::operation::list_organisation::ListOrganisationInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder + } +} + +impl From for crate::types::ListOrganisationOutput { + fn from(sdk: superposition_sdk::operation::list_organisation::ListOrganisationOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_secrets::builders::ListSecretsInputBuilder { + fn from(input: crate::types::ListSecretsInput) -> Self { + let mut builder = superposition_sdk::operation::list_secrets::ListSecretsInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(input.name); + builder = builder.set_created_by(input.created_by); + builder = builder.set_last_modified_by(input.last_modified_by); + builder = builder.set_sort_on(input.sort_on.map(Into::into)); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder + } +} + +impl From for crate::types::ListSecretsOutput { + fn from(sdk: superposition_sdk::operation::list_secrets::ListSecretsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_variables::builders::ListVariablesInputBuilder { + fn from(input: crate::types::ListVariablesInput) -> Self { + let mut builder = superposition_sdk::operation::list_variables::ListVariablesInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(input.name); + builder = builder.set_created_by(input.created_by); + builder = builder.set_last_modified_by(input.last_modified_by); + builder = builder.set_sort_on(input.sort_on.map(Into::into)); + builder = builder.set_sort_by(input.sort_by.map(Into::into)); + builder + } +} + +impl From for crate::types::ListVariablesOutput { + fn from(sdk: superposition_sdk::operation::list_variables::ListVariablesOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_versions::builders::ListVersionsInputBuilder { + fn from(input: crate::types::ListVersionsInput) -> Self { + let mut builder = superposition_sdk::operation::list_versions::ListVersionsInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder + } +} + +impl From for crate::types::ListVersionsOutput { + fn from(sdk: superposition_sdk::operation::list_versions::ListVersionsOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_webhook::builders::ListWebhookInputBuilder { + fn from(input: crate::types::ListWebhookInput) -> Self { + let mut builder = superposition_sdk::operation::list_webhook::ListWebhookInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder + } +} + +impl From for crate::types::ListWebhookOutput { + fn from(sdk: superposition_sdk::operation::list_webhook::ListWebhookOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::list_workspace::builders::ListWorkspaceInputBuilder { + fn from(input: crate::types::ListWorkspaceInput) -> Self { + let mut builder = superposition_sdk::operation::list_workspace::ListWorkspaceInput::builder(); + builder = builder.set_count(input.count); + builder = builder.set_page(input.page); + builder = builder.set_all(input.all); + builder = builder.set_org_id(Some(input.org_id)); + builder + } +} + +impl From for crate::types::ListWorkspaceOutput { + fn from(sdk: superposition_sdk::operation::list_workspace::ListWorkspaceOutput) -> Self { + Self { + total_pages: sdk.total_pages, + total_items: sdk.total_items, + data: sdk.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for superposition_sdk::operation::migrate_workspace_schema::builders::MigrateWorkspaceSchemaInputBuilder { + fn from(input: crate::types::WorkspaceSelectorRequest) -> Self { + let mut builder = superposition_sdk::operation::migrate_workspace_schema::MigrateWorkspaceSchemaInput::builder(); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_workspace_name(Some(input.workspace_name)); + builder + } +} + +impl From for crate::types::WorkspaceResponse { + fn from(sdk: superposition_sdk::operation::migrate_workspace_schema::MigrateWorkspaceSchemaOutput) -> Self { + Self { + workspace_name: sdk.workspace_name, + organisation_id: sdk.organisation_id, + organisation_name: sdk.organisation_name, + workspace_schema_name: sdk.workspace_schema_name, + workspace_status: sdk.workspace_status.into(), + workspace_admin_email: sdk.workspace_admin_email, + config_version: sdk.config_version, + created_by: sdk.created_by, + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + mandatory_dimensions: sdk.mandatory_dimensions, + metrics: smithy_mcp_runtime::document_to_value(sdk.metrics), + allow_experiment_self_approval: sdk.allow_experiment_self_approval, + auto_populate_control: sdk.auto_populate_control, + enable_context_validation: sdk.enable_context_validation, + enable_change_reason_validation: sdk.enable_change_reason_validation, + } + } +} + +impl From for superposition_sdk::operation::move_context::builders::MoveContextInputBuilder { + fn from(input: crate::types::MoveContextInput) -> Self { + let mut builder = superposition_sdk::operation::move_context::MoveContextInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_request(Some(input.request.into())); + builder + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::operation::move_context::MoveContextOutput) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::pause_experiment::builders::PauseExperimentInputBuilder { + fn from(input: crate::types::PauseExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::pause_experiment::PauseExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::pause_experiment::PauseExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::publish::builders::PublishInputBuilder { + fn from(input: crate::types::PublishInput) -> Self { + let mut builder = superposition_sdk::operation::publish::PublishInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::FunctionResponse { + fn from(sdk: superposition_sdk::operation::publish::PublishOutput) -> Self { + Self { + function_name: sdk.function_name, + published_code: sdk.published_code, + draft_code: sdk.draft_code, + published_runtime_version: sdk.published_runtime_version.map(Into::into), + draft_runtime_version: sdk.draft_runtime_version.into(), + published_at: sdk.published_at.map(smithy_mcp_runtime::datetime_to_string), + draft_edited_at: smithy_mcp_runtime::datetime_to_string(sdk.draft_edited_at), + published_by: sdk.published_by, + draft_edited_by: sdk.draft_edited_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + change_reason: sdk.change_reason, + description: sdk.description, + function_type: sdk.function_type.into(), + } + } +} + +impl From for superposition_sdk::operation::ramp_experiment::builders::RampExperimentInputBuilder { + fn from(input: crate::types::RampExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::ramp_experiment::RampExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_traffic_percentage(Some(input.traffic_percentage)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::ramp_experiment::RampExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::remove_members_from_group::builders::RemoveMembersFromGroupInputBuilder { + fn from(input: crate::types::ModifyMembersToGroupRequest) -> Self { + let mut builder = superposition_sdk::operation::remove_members_from_group::RemoveMembersFromGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_member_experiment_ids(Some(input.member_experiment_ids)); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::remove_members_from_group::RemoveMembersFromGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::resume_experiment::builders::ResumeExperimentInputBuilder { + fn from(input: crate::types::ResumeExperimentInput) -> Self { + let mut builder = superposition_sdk::operation::resume_experiment::ResumeExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::resume_experiment::ResumeExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::rotate_master_encryption_key::builders::RotateMasterEncryptionKeyInputBuilder { + fn from(input: crate::types::Unit) -> Self { + let mut builder = superposition_sdk::operation::rotate_master_encryption_key::RotateMasterEncryptionKeyInput::builder(); + builder + } +} + +impl From for crate::types::RotateMasterEncryptionKeyOutput { + fn from(sdk: superposition_sdk::operation::rotate_master_encryption_key::RotateMasterEncryptionKeyOutput) -> Self { + Self { + workspaces_rotated: sdk.workspaces_rotated, + total_secrets_re_encrypted: sdk.total_secrets_re_encrypted, + } + } +} + +impl From for superposition_sdk::operation::rotate_workspace_encryption_key::builders::RotateWorkspaceEncryptionKeyInputBuilder { + fn from(input: crate::types::WorkspaceSelectorRequest) -> Self { + let mut builder = superposition_sdk::operation::rotate_workspace_encryption_key::RotateWorkspaceEncryptionKeyInput::builder(); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_workspace_name(Some(input.workspace_name)); + builder + } +} + +impl From for crate::types::RotateWorkspaceEncryptionKeyOutput { + fn from(sdk: superposition_sdk::operation::rotate_workspace_encryption_key::RotateWorkspaceEncryptionKeyOutput) -> Self { + Self { + total_secrets_re_encrypted: sdk.total_secrets_re_encrypted, + } + } +} + +impl From for superposition_sdk::operation::test::builders::TestInputBuilder { + fn from(input: crate::types::TestInput) -> Self { + let mut builder = superposition_sdk::operation::test::TestInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder = builder.set_stage(Some(input.stage.into())); + builder = builder.set_request(Some(input.request.into())); + builder + } +} + +impl From for crate::types::FunctionExecutionResponse { + fn from(sdk: superposition_sdk::operation::test::TestOutput) -> Self { + Self { + fn_output: smithy_mcp_runtime::document_to_value(sdk.fn_output), + stdout: sdk.stdout, + function_type: sdk.function_type.into(), + } + } +} + +impl From for superposition_sdk::operation::update_default_config::builders::UpdateDefaultConfigInputBuilder { + fn from(input: crate::types::UpdateDefaultConfigInput) -> Self { + let mut builder = superposition_sdk::operation::update_default_config::UpdateDefaultConfigInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_key(Some(input.key)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_value(input.value.map(smithy_mcp_runtime::value_to_document)); + builder = builder.set_schema(input.schema.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_value_validation_function_name(input.value_validation_function_name); + builder = builder.set_description(input.description); + builder = builder.set_value_compute_function_name(input.value_compute_function_name); + builder + } +} + +impl From for crate::types::DefaultConfigResponse { + fn from(sdk: superposition_sdk::operation::update_default_config::UpdateDefaultConfigOutput) -> Self { + Self { + key: sdk.key, + value: smithy_mcp_runtime::document_to_value(sdk.value), + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + value_validation_function_name: sdk.value_validation_function_name, + value_compute_function_name: sdk.value_compute_function_name, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::update_dimension::builders::UpdateDimensionInputBuilder { + fn from(input: crate::types::UpdateDimensionInput) -> Self { + let mut builder = superposition_sdk::operation::update_dimension::UpdateDimensionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_dimension(Some(input.dimension)); + builder = builder.set_schema(input.schema.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_position(input.position); + builder = builder.set_value_validation_function_name(input.value_validation_function_name); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_value_compute_function_name(input.value_compute_function_name); + builder + } +} + +impl From for crate::types::DimensionResponse { + fn from(sdk: superposition_sdk::operation::update_dimension::UpdateDimensionOutput) -> Self { + Self { + dimension: sdk.dimension, + position: sdk.position, + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + value_validation_function_name: sdk.value_validation_function_name, + description: sdk.description, + change_reason: sdk.change_reason, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + dependency_graph: sdk.dependency_graph, + dimension_type: sdk.dimension_type.into(), + value_compute_function_name: sdk.value_compute_function_name, + mandatory: sdk.mandatory, + } + } +} + +impl From for superposition_sdk::operation::update_experiment_group::builders::UpdateExperimentGroupInputBuilder { + fn from(input: crate::types::UpdateExperimentGroupRequest) -> Self { + let mut builder = superposition_sdk::operation::update_experiment_group::UpdateExperimentGroupInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_description(input.description); + builder = builder.set_traffic_percentage(input.traffic_percentage); + builder + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::operation::update_experiment_group::UpdateExperimentGroupOutput) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for superposition_sdk::operation::update_function::builders::UpdateFunctionInputBuilder { + fn from(input: crate::types::UpdateFunctionRequest) -> Self { + let mut builder = superposition_sdk::operation::update_function::UpdateFunctionInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_function_name(Some(input.function_name)); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_function(input.function); + builder = builder.set_runtime_version(input.runtime_version.map(Into::into)); + builder + } +} + +impl From for crate::types::FunctionResponse { + fn from(sdk: superposition_sdk::operation::update_function::UpdateFunctionOutput) -> Self { + Self { + function_name: sdk.function_name, + published_code: sdk.published_code, + draft_code: sdk.draft_code, + published_runtime_version: sdk.published_runtime_version.map(Into::into), + draft_runtime_version: sdk.draft_runtime_version.into(), + published_at: sdk.published_at.map(smithy_mcp_runtime::datetime_to_string), + draft_edited_at: smithy_mcp_runtime::datetime_to_string(sdk.draft_edited_at), + published_by: sdk.published_by, + draft_edited_by: sdk.draft_edited_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + change_reason: sdk.change_reason, + description: sdk.description, + function_type: sdk.function_type.into(), + } + } +} + +impl From for superposition_sdk::operation::update_organisation::builders::UpdateOrganisationInputBuilder { + fn from(input: crate::types::UpdateOrganisationRequest) -> Self { + let mut builder = superposition_sdk::operation::update_organisation::UpdateOrganisationInput::builder(); + builder = builder.set_country_code(input.country_code); + builder = builder.set_contact_email(input.contact_email); + builder = builder.set_contact_phone(input.contact_phone); + builder = builder.set_admin_email(input.admin_email); + builder = builder.set_sector(input.sector); + builder = builder.set_id(Some(input.id)); + builder = builder.set_status(input.status.map(Into::into)); + builder + } +} + +impl From for crate::types::OrganisationResponse { + fn from(sdk: superposition_sdk::operation::update_organisation::UpdateOrganisationOutput) -> Self { + Self { + id: sdk.id, + name: sdk.name, + country_code: sdk.country_code, + contact_email: sdk.contact_email, + contact_phone: sdk.contact_phone, + created_by: sdk.created_by, + admin_email: sdk.admin_email, + status: sdk.status.into(), + sector: sdk.sector, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + updated_at: smithy_mcp_runtime::datetime_to_string(sdk.updated_at), + updated_by: sdk.updated_by, + } + } +} + +impl From for superposition_sdk::operation::update_override::builders::UpdateOverrideInputBuilder { + fn from(input: crate::types::UpdateOverrideInput) -> Self { + let mut builder = superposition_sdk::operation::update_override::UpdateOverrideInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_config_tags(input.config_tags); + builder = builder.set_request(Some(input.request.into())); + builder + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::operation::update_override::UpdateOverrideOutput) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::update_overrides_experiment::builders::UpdateOverridesExperimentInputBuilder { + fn from(input: crate::types::UpdateOverrideRequest) -> Self { + let mut builder = superposition_sdk::operation::update_overrides_experiment::UpdateOverridesExperimentInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_id(Some(input.id)); + builder = builder.set_variant_list(Some(input.variant_list.into_iter().map(Into::into).collect())); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder = builder.set_metrics(input.metrics.map(smithy_mcp_runtime::value_to_document)); + builder = builder.set_experiment_group_id(input.experiment_group_id); + builder + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::operation::update_overrides_experiment::UpdateOverridesExperimentOutput) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for superposition_sdk::operation::update_secret::builders::UpdateSecretInputBuilder { + fn from(input: crate::types::UpdateSecretInput) -> Self { + let mut builder = superposition_sdk::operation::update_secret::UpdateSecretInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_value(input.value); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::SecretResponse { + fn from(sdk: superposition_sdk::operation::update_secret::UpdateSecretOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::update_type_templates::builders::UpdateTypeTemplatesInputBuilder { + fn from(input: crate::types::UpdateTypeTemplatesRequest) -> Self { + let mut builder = superposition_sdk::operation::update_type_templates::UpdateTypeTemplatesInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_type_name(Some(input.type_name)); + builder = builder.set_type_schema(Some(input.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::TypeTemplatesResponse { + fn from(sdk: superposition_sdk::operation::update_type_templates::UpdateTypeTemplatesOutput) -> Self { + Self { + type_name: sdk.type_name, + type_schema: sdk.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for superposition_sdk::operation::update_variable::builders::UpdateVariableInputBuilder { + fn from(input: crate::types::UpdateVariableInput) -> Self { + let mut builder = superposition_sdk::operation::update_variable::UpdateVariableInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_value(input.value); + builder = builder.set_description(input.description); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::VariableResponse { + fn from(sdk: superposition_sdk::operation::update_variable::UpdateVariableOutput) -> Self { + Self { + name: sdk.name, + value: sdk.value, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::update_webhook::builders::UpdateWebhookInputBuilder { + fn from(input: crate::types::UpdateWebhookInput) -> Self { + let mut builder = superposition_sdk::operation::update_webhook::UpdateWebhookInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_name(Some(input.name)); + builder = builder.set_description(input.description); + builder = builder.set_enabled(input.enabled); + builder = builder.set_url(input.url); + builder = builder.set_method(input.method.map(Into::into)); + builder = builder.set_version(input.version.map(Into::into)); + builder = builder.set_custom_headers(input.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder = builder.set_events(input.events); + builder = builder.set_change_reason(Some(input.change_reason)); + builder + } +} + +impl From for crate::types::WebhookResponse { + fn from(sdk: superposition_sdk::operation::update_webhook::UpdateWebhookOutput) -> Self { + Self { + name: sdk.name, + description: sdk.description, + enabled: sdk.enabled, + url: sdk.url, + method: sdk.method.into(), + version: sdk.version.into(), + custom_headers: sdk.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect()), + events: sdk.events, + max_retries: sdk.max_retries, + last_triggered_at: sdk.last_triggered_at.map(smithy_mcp_runtime::datetime_to_string), + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for superposition_sdk::operation::update_workspace::builders::UpdateWorkspaceInputBuilder { + fn from(input: crate::types::UpdateWorkspaceRequest) -> Self { + let mut builder = superposition_sdk::operation::update_workspace::UpdateWorkspaceInput::builder(); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_workspace_name(Some(input.workspace_name)); + builder = builder.set_workspace_admin_email(input.workspace_admin_email); + builder = builder.set_config_version(input.config_version); + builder = builder.set_mandatory_dimensions(input.mandatory_dimensions); + builder = builder.set_workspace_status(input.workspace_status.map(Into::into)); + builder = builder.set_metrics(input.metrics.map(smithy_mcp_runtime::value_to_document)); + builder = builder.set_allow_experiment_self_approval(input.allow_experiment_self_approval); + builder = builder.set_auto_populate_control(input.auto_populate_control); + builder = builder.set_enable_context_validation(input.enable_context_validation); + builder = builder.set_enable_change_reason_validation(input.enable_change_reason_validation); + builder + } +} + +impl From for crate::types::WorkspaceResponse { + fn from(sdk: superposition_sdk::operation::update_workspace::UpdateWorkspaceOutput) -> Self { + Self { + workspace_name: sdk.workspace_name, + organisation_id: sdk.organisation_id, + organisation_name: sdk.organisation_name, + workspace_schema_name: sdk.workspace_schema_name, + workspace_status: sdk.workspace_status.into(), + workspace_admin_email: sdk.workspace_admin_email, + config_version: sdk.config_version, + created_by: sdk.created_by, + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + mandatory_dimensions: sdk.mandatory_dimensions, + metrics: smithy_mcp_runtime::document_to_value(sdk.metrics), + allow_experiment_self_approval: sdk.allow_experiment_self_approval, + auto_populate_control: sdk.auto_populate_control, + enable_context_validation: sdk.enable_context_validation, + enable_change_reason_validation: sdk.enable_change_reason_validation, + } + } +} + +impl From for superposition_sdk::operation::validate_context::builders::ValidateContextInputBuilder { + fn from(input: crate::types::ValidateContextInput) -> Self { + let mut builder = superposition_sdk::operation::validate_context::ValidateContextInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::operation::validate_context::ValidateContextOutput) -> Self { + Self { + } + } +} + +impl From for superposition_sdk::operation::weight_recompute::builders::WeightRecomputeInputBuilder { + fn from(input: crate::types::WeightRecomputeInput) -> Self { + let mut builder = superposition_sdk::operation::weight_recompute::WeightRecomputeInput::builder(); + builder = builder.set_workspace_id(Some(input.workspace_id)); + builder = builder.set_org_id(Some(input.org_id)); + builder = builder.set_config_tags(input.config_tags); + builder + } +} + +impl From for crate::types::WeightRecomputeOutput { + fn from(sdk: superposition_sdk::operation::weight_recompute::WeightRecomputeOutput) -> Self { + Self { + data: sdk.data.map(|v| v.into_iter().map(Into::into).collect()), + } + } +} + +impl From for superposition_sdk::types::ContextPut { + fn from(input: crate::types::ContextPut) -> Self { + let builder = superposition_sdk::types::ContextPut::builder(); + let builder = builder + .set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())) + .set_override(Some(input.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())) + .set_description(input.description) + .set_change_reason(Some(input.change_reason)); + builder.build().expect("ContextPut build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::UpdateContextOverrideRequest { + fn from(input: crate::types::UpdateContextOverrideRequest) -> Self { + let builder = superposition_sdk::types::UpdateContextOverrideRequest::builder(); + let builder = builder + .set_context(Some(input.context.into())) + .set_override(Some(input.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())) + .set_description(input.description) + .set_change_reason(Some(input.change_reason)); + builder.build().expect("UpdateContextOverrideRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ContextMoveBulkRequest { + fn from(input: crate::types::ContextMoveBulkRequest) -> Self { + let builder = superposition_sdk::types::ContextMoveBulkRequest::builder(); + let builder = builder + .set_id(Some(input.id)) + .set_request(Some(input.request.into())); + builder.build().expect("ContextMoveBulkRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ContextMove { + fn from(input: crate::types::ContextMove) -> Self { + let builder = superposition_sdk::types::ContextMove::builder(); + let builder = builder + .set_context(Some(input.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())) + .set_description(input.description) + .set_change_reason(Some(input.change_reason)); + builder.build().expect("ContextMove build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::Unit { + fn from(input: crate::types::Unit) -> Self { + let builder = superposition_sdk::types::Unit::builder(); + // no members — empty struct + builder.build() + } +} + +impl From for superposition_sdk::types::Variant { + fn from(input: crate::types::Variant) -> Self { + let builder = superposition_sdk::types::Variant::builder(); + let builder = builder + .set_id(Some(input.id)) + .set_variant_type(Some(input.variant_type.into())) + .set_context_id(input.context_id) + .set_override_id(input.override_id) + .set_overrides(Some(input.overrides.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder.build().expect("Variant build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ValueValidationFunctionRequest { + fn from(input: crate::types::ValueValidationFunctionRequest) -> Self { + let builder = superposition_sdk::types::ValueValidationFunctionRequest::builder(); + let builder = builder + .set_key(Some(input.key)) + .set_value(Some(smithy_mcp_runtime::value_to_document(input.value))) + .set_type(Some(input.r#type)) + .set_environment(Some(smithy_mcp_runtime::value_to_document(input.environment))); + builder.build().expect("ValueValidationFunctionRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ValueComputeFunctionRequest { + fn from(input: crate::types::ValueComputeFunctionRequest) -> Self { + let builder = superposition_sdk::types::ValueComputeFunctionRequest::builder(); + let builder = builder + .set_name(Some(input.name)) + .set_prefix(Some(input.prefix)) + .set_type(Some(input.r#type)) + .set_environment(Some(smithy_mcp_runtime::value_to_document(input.environment))); + builder.build().expect("ValueComputeFunctionRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ContextValidationFunctionRequest { + fn from(input: crate::types::ContextValidationFunctionRequest) -> Self { + let builder = superposition_sdk::types::ContextValidationFunctionRequest::builder(); + let builder = builder + .set_environment(Some(smithy_mcp_runtime::value_to_document(input.environment))); + builder.build().expect("ContextValidationFunctionRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::ChangeReasonValidationFunctionRequest { + fn from(input: crate::types::ChangeReasonValidationFunctionRequest) -> Self { + let builder = superposition_sdk::types::ChangeReasonValidationFunctionRequest::builder(); + let builder = builder + .set_change_reason(Some(input.change_reason)); + builder.build().expect("ChangeReasonValidationFunctionRequest build failed — required field missing in MCP-side type") + } +} + +impl From for superposition_sdk::types::VariantUpdateRequest { + fn from(input: crate::types::VariantUpdateRequest) -> Self { + let builder = superposition_sdk::types::VariantUpdateRequest::builder(); + let builder = builder + .set_id(Some(input.id)) + .set_overrides(Some(input.overrides.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect())); + builder.build().expect("VariantUpdateRequest build failed — required field missing in MCP-side type") + } +} + +impl From for crate::types::Bucket { + fn from(sdk: superposition_sdk::types::Bucket) -> Self { + Self { + experiment_id: sdk.experiment_id, + variant_id: sdk.variant_id, + } + } +} + +impl From for crate::types::Variant { + fn from(sdk: superposition_sdk::types::Variant) -> Self { + Self { + id: sdk.id, + variant_type: sdk.variant_type.into(), + context_id: sdk.context_id, + override_id: sdk.override_id, + overrides: sdk.overrides.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + } + } +} + +impl From for crate::types::ContextResponse { + fn from(sdk: superposition_sdk::types::ContextResponse) -> Self { + Self { + id: sdk.id, + value: sdk.value.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + r#override: sdk.r#override.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + override_id: sdk.override_id, + weight: sdk.weight, + description: sdk.description, + change_reason: sdk.change_reason, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for crate::types::Unit { + fn from(sdk: superposition_sdk::types::Unit) -> Self { + Self { + } + } +} + +impl From for crate::types::ContextPartial { + fn from(sdk: superposition_sdk::types::ContextPartial) -> Self { + Self { + id: sdk.id, + condition: sdk.condition.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + priority: sdk.priority, + weight: sdk.weight, + override_with_keys: sdk.override_with_keys, + } + } +} + +impl From for crate::types::DimensionInfo { + fn from(sdk: superposition_sdk::types::DimensionInfo) -> Self { + Self { + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + position: sdk.position, + dimension_type: sdk.dimension_type.into(), + dependency_graph: sdk.dependency_graph, + value_compute_function_name: sdk.value_compute_function_name, + } + } +} + +impl From for crate::types::ExperimentResponse { + fn from(sdk: superposition_sdk::types::ExperimentResponse) -> Self { + Self { + id: sdk.id, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified: smithy_mcp_runtime::datetime_to_string(sdk.last_modified), + name: sdk.name, + experiment_type: sdk.experiment_type.into(), + override_keys: sdk.override_keys, + status: sdk.status.into(), + traffic_percentage: sdk.traffic_percentage, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + variants: sdk.variants.into_iter().map(Into::into).collect(), + last_modified_by: sdk.last_modified_by, + chosen_variant: sdk.chosen_variant, + description: sdk.description, + change_reason: sdk.change_reason, + started_at: sdk.started_at.map(smithy_mcp_runtime::datetime_to_string), + started_by: sdk.started_by, + metrics_url: sdk.metrics_url, + metrics: sdk.metrics.map(smithy_mcp_runtime::document_to_value), + experiment_group_id: sdk.experiment_group_id, + } + } +} + +impl From for crate::types::ExperimentGroupResponse { + fn from(sdk: superposition_sdk::types::ExperimentGroupResponse) -> Self { + Self { + id: sdk.id, + context_hash: sdk.context_hash, + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + context: sdk.context.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + traffic_percentage: sdk.traffic_percentage, + member_experiment_ids: sdk.member_experiment_ids, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + buckets: sdk.buckets.into_iter().filter_map(|o| o.map(Into::into)).collect(), + group_type: sdk.group_type.into(), + } + } +} + +impl From for crate::types::TypeTemplatesResponse { + fn from(sdk: superposition_sdk::types::TypeTemplatesResponse) -> Self { + Self { + type_name: sdk.type_name, + type_schema: sdk.type_schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for crate::types::ConfigData { + fn from(sdk: superposition_sdk::types::ConfigData) -> Self { + Self { + contexts: sdk.contexts.into_iter().map(Into::into).collect(), + overrides: sdk.overrides.into_iter().map(|(k, v)| (k, v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect())).collect(), + default_configs: sdk.default_configs.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + dimensions: sdk.dimensions.into_iter().map(|(k, v)| (k, v.into())).collect(), + } + } +} + +impl From for crate::types::AuditLogFull { + fn from(sdk: superposition_sdk::types::AuditLogFull) -> Self { + Self { + id: sdk.id, + table_name: sdk.table_name, + user_name: sdk.user_name, + timestamp: smithy_mcp_runtime::datetime_to_string(sdk.timestamp), + action: sdk.action.into(), + original_data: sdk.original_data.map(smithy_mcp_runtime::document_to_value), + new_data: sdk.new_data.map(smithy_mcp_runtime::document_to_value), + query: sdk.query, + } + } +} + +impl From for crate::types::DefaultConfigResponse { + fn from(sdk: superposition_sdk::types::DefaultConfigResponse) -> Self { + Self { + key: sdk.key, + value: smithy_mcp_runtime::document_to_value(sdk.value), + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + description: sdk.description, + change_reason: sdk.change_reason, + value_validation_function_name: sdk.value_validation_function_name, + value_compute_function_name: sdk.value_compute_function_name, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + } + } +} + +impl From for crate::types::DimensionResponse { + fn from(sdk: superposition_sdk::types::DimensionResponse) -> Self { + Self { + dimension: sdk.dimension, + position: sdk.position, + schema: sdk.schema.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + value_validation_function_name: sdk.value_validation_function_name, + description: sdk.description, + change_reason: sdk.change_reason, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + created_by: sdk.created_by, + dependency_graph: sdk.dependency_graph, + dimension_type: sdk.dimension_type.into(), + value_compute_function_name: sdk.value_compute_function_name, + mandatory: sdk.mandatory, + } + } +} + +impl From for crate::types::FunctionResponse { + fn from(sdk: superposition_sdk::types::FunctionResponse) -> Self { + Self { + function_name: sdk.function_name, + published_code: sdk.published_code, + draft_code: sdk.draft_code, + published_runtime_version: sdk.published_runtime_version.map(Into::into), + draft_runtime_version: sdk.draft_runtime_version.into(), + published_at: sdk.published_at.map(smithy_mcp_runtime::datetime_to_string), + draft_edited_at: smithy_mcp_runtime::datetime_to_string(sdk.draft_edited_at), + published_by: sdk.published_by, + draft_edited_by: sdk.draft_edited_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + last_modified_by: sdk.last_modified_by, + change_reason: sdk.change_reason, + description: sdk.description, + function_type: sdk.function_type.into(), + } + } +} + +impl From for crate::types::OrganisationResponse { + fn from(sdk: superposition_sdk::types::OrganisationResponse) -> Self { + Self { + id: sdk.id, + name: sdk.name, + country_code: sdk.country_code, + contact_email: sdk.contact_email, + contact_phone: sdk.contact_phone, + created_by: sdk.created_by, + admin_email: sdk.admin_email, + status: sdk.status.into(), + sector: sdk.sector, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + updated_at: smithy_mcp_runtime::datetime_to_string(sdk.updated_at), + updated_by: sdk.updated_by, + } + } +} + +impl From for crate::types::SecretResponse { + fn from(sdk: superposition_sdk::types::SecretResponse) -> Self { + Self { + name: sdk.name, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for crate::types::VariableResponse { + fn from(sdk: superposition_sdk::types::VariableResponse) -> Self { + Self { + name: sdk.name, + value: sdk.value, + description: sdk.description, + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for crate::types::ListVersionsMember { + fn from(sdk: superposition_sdk::types::ListVersionsMember) -> Self { + Self { + id: sdk.id, + config: sdk.config.into(), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + description: sdk.description, + tags: sdk.tags, + } + } +} + +impl From for crate::types::WebhookResponse { + fn from(sdk: superposition_sdk::types::WebhookResponse) -> Self { + Self { + name: sdk.name, + description: sdk.description, + enabled: sdk.enabled, + url: sdk.url, + method: sdk.method.into(), + version: sdk.version.into(), + custom_headers: sdk.custom_headers.map(|v| v.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect()), + events: sdk.events, + max_retries: sdk.max_retries, + last_triggered_at: sdk.last_triggered_at.map(smithy_mcp_runtime::datetime_to_string), + change_reason: sdk.change_reason, + created_by: sdk.created_by, + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + } + } +} + +impl From for crate::types::WorkspaceResponse { + fn from(sdk: superposition_sdk::types::WorkspaceResponse) -> Self { + Self { + workspace_name: sdk.workspace_name, + organisation_id: sdk.organisation_id, + organisation_name: sdk.organisation_name, + workspace_schema_name: sdk.workspace_schema_name, + workspace_status: sdk.workspace_status.into(), + workspace_admin_email: sdk.workspace_admin_email, + config_version: sdk.config_version, + created_by: sdk.created_by, + last_modified_by: sdk.last_modified_by, + last_modified_at: smithy_mcp_runtime::datetime_to_string(sdk.last_modified_at), + created_at: smithy_mcp_runtime::datetime_to_string(sdk.created_at), + mandatory_dimensions: sdk.mandatory_dimensions, + metrics: smithy_mcp_runtime::document_to_value(sdk.metrics), + allow_experiment_self_approval: sdk.allow_experiment_self_approval, + auto_populate_control: sdk.auto_populate_control, + enable_context_validation: sdk.enable_context_validation, + enable_change_reason_validation: sdk.enable_change_reason_validation, + } + } +} + +impl From for crate::types::WeightRecomputeResponse { + fn from(sdk: superposition_sdk::types::WeightRecomputeResponse) -> Self { + Self { + id: sdk.id, + condition: sdk.condition.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::document_to_value(v))).collect(), + old_weight: sdk.old_weight, + new_weight: sdk.new_weight, + } + } +} + +impl From for superposition_sdk::types::ExperimentType { + fn from(mcp: crate::types::ExperimentType) -> Self { + match mcp { + crate::types::ExperimentType::Default => superposition_sdk::types::ExperimentType::Default, + crate::types::ExperimentType::DeleteOverrides => superposition_sdk::types::ExperimentType::DeleteOverrides, + } + } +} + +impl From for superposition_sdk::types::VariantType { + fn from(mcp: crate::types::VariantType) -> Self { + match mcp { + crate::types::VariantType::Control => superposition_sdk::types::VariantType::Control, + crate::types::VariantType::Experimental => superposition_sdk::types::VariantType::Experimental, + } + } +} + +impl From for superposition_sdk::types::FunctionRuntimeVersion { + fn from(mcp: crate::types::FunctionRuntimeVersion) -> Self { + match mcp { + crate::types::FunctionRuntimeVersion::V1 => superposition_sdk::types::FunctionRuntimeVersion::V1, + } + } +} + +impl From for superposition_sdk::types::FunctionTypes { + fn from(mcp: crate::types::FunctionTypes) -> Self { + match mcp { + crate::types::FunctionTypes::ValueValidation => superposition_sdk::types::FunctionTypes::ValueValidation, + crate::types::FunctionTypes::ValueCompute => superposition_sdk::types::FunctionTypes::ValueCompute, + crate::types::FunctionTypes::ContextValidation => superposition_sdk::types::FunctionTypes::ContextValidation, + crate::types::FunctionTypes::ChangeReasonValidation => superposition_sdk::types::FunctionTypes::ChangeReasonValidation, + } + } +} + +impl From for superposition_sdk::types::HttpMethod { + fn from(mcp: crate::types::HttpMethod) -> Self { + match mcp { + crate::types::HttpMethod::Get => superposition_sdk::types::HttpMethod::Get, + crate::types::HttpMethod::Post => superposition_sdk::types::HttpMethod::Post, + crate::types::HttpMethod::Put => superposition_sdk::types::HttpMethod::Put, + crate::types::HttpMethod::Patch => superposition_sdk::types::HttpMethod::Patch, + crate::types::HttpMethod::Delete => superposition_sdk::types::HttpMethod::Delete, + crate::types::HttpMethod::Head => superposition_sdk::types::HttpMethod::Head, + } + } +} + +impl From for superposition_sdk::types::Version { + fn from(mcp: crate::types::Version) -> Self { + match mcp { + crate::types::Version::V1 => superposition_sdk::types::Version::V1, + } + } +} + +impl From for superposition_sdk::types::WorkspaceStatus { + fn from(mcp: crate::types::WorkspaceStatus) -> Self { + match mcp { + crate::types::WorkspaceStatus::Enabled => superposition_sdk::types::WorkspaceStatus::Enabled, + crate::types::WorkspaceStatus::Disabled => superposition_sdk::types::WorkspaceStatus::Disabled, + } + } +} + +impl From for superposition_sdk::types::DimensionMatchStrategy { + fn from(mcp: crate::types::DimensionMatchStrategy) -> Self { + match mcp { + crate::types::DimensionMatchStrategy::Exact => superposition_sdk::types::DimensionMatchStrategy::Exact, + crate::types::DimensionMatchStrategy::Subset => superposition_sdk::types::DimensionMatchStrategy::Subset, + } + } +} + +impl From for superposition_sdk::types::MergeStrategy { + fn from(mcp: crate::types::MergeStrategy) -> Self { + match mcp { + crate::types::MergeStrategy::Merge => superposition_sdk::types::MergeStrategy::Merge, + crate::types::MergeStrategy::Replace => superposition_sdk::types::MergeStrategy::Replace, + } + } +} + +impl From for superposition_sdk::types::AuditAction { + fn from(mcp: crate::types::AuditAction) -> Self { + match mcp { + crate::types::AuditAction::Insert => superposition_sdk::types::AuditAction::Insert, + crate::types::AuditAction::Update => superposition_sdk::types::AuditAction::Update, + crate::types::AuditAction::Delete => superposition_sdk::types::AuditAction::Delete, + } + } +} + +impl From for superposition_sdk::types::SortBy { + fn from(mcp: crate::types::SortBy) -> Self { + match mcp { + crate::types::SortBy::Desc => superposition_sdk::types::SortBy::Desc, + crate::types::SortBy::Asc => superposition_sdk::types::SortBy::Asc, + } + } +} + +impl From for superposition_sdk::types::ContextFilterSortOn { + fn from(mcp: crate::types::ContextFilterSortOn) -> Self { + match mcp { + crate::types::ContextFilterSortOn::LastModifiedAt => superposition_sdk::types::ContextFilterSortOn::LastModifiedAt, + crate::types::ContextFilterSortOn::CreatedAt => superposition_sdk::types::ContextFilterSortOn::CreatedAt, + crate::types::ContextFilterSortOn::Weight => superposition_sdk::types::ContextFilterSortOn::Weight, + } + } +} + +impl From for superposition_sdk::types::ExperimentStatusType { + fn from(mcp: crate::types::ExperimentStatusType) -> Self { + match mcp { + crate::types::ExperimentStatusType::Created => superposition_sdk::types::ExperimentStatusType::Created, + crate::types::ExperimentStatusType::Concluded => superposition_sdk::types::ExperimentStatusType::Concluded, + crate::types::ExperimentStatusType::Inprogress => superposition_sdk::types::ExperimentStatusType::Inprogress, + crate::types::ExperimentStatusType::Discarded => superposition_sdk::types::ExperimentStatusType::Discarded, + crate::types::ExperimentStatusType::Paused => superposition_sdk::types::ExperimentStatusType::Paused, + } + } +} + +impl From for superposition_sdk::types::ExperimentSortOn { + fn from(mcp: crate::types::ExperimentSortOn) -> Self { + match mcp { + crate::types::ExperimentSortOn::LastModifiedAt => superposition_sdk::types::ExperimentSortOn::LastModifiedAt, + crate::types::ExperimentSortOn::CreatedAt => superposition_sdk::types::ExperimentSortOn::CreatedAt, + } + } +} + +impl From for superposition_sdk::types::ExperimentGroupSortOn { + fn from(mcp: crate::types::ExperimentGroupSortOn) -> Self { + match mcp { + crate::types::ExperimentGroupSortOn::Name => superposition_sdk::types::ExperimentGroupSortOn::Name, + crate::types::ExperimentGroupSortOn::CreatedAt => superposition_sdk::types::ExperimentGroupSortOn::CreatedAt, + crate::types::ExperimentGroupSortOn::LastModifiedAt => superposition_sdk::types::ExperimentGroupSortOn::LastModifiedAt, + } + } +} + +impl From for superposition_sdk::types::GroupType { + fn from(mcp: crate::types::GroupType) -> Self { + match mcp { + crate::types::GroupType::UserCreated => superposition_sdk::types::GroupType::UserCreated, + crate::types::GroupType::SystemGenerated => superposition_sdk::types::GroupType::SystemGenerated, + } + } +} + +impl From for superposition_sdk::types::SecretSortOn { + fn from(mcp: crate::types::SecretSortOn) -> Self { + match mcp { + crate::types::SecretSortOn::Name => superposition_sdk::types::SecretSortOn::Name, + crate::types::SecretSortOn::CreatedAt => superposition_sdk::types::SecretSortOn::CreatedAt, + crate::types::SecretSortOn::LastModifiedAt => superposition_sdk::types::SecretSortOn::LastModifiedAt, + } + } +} + +impl From for superposition_sdk::types::VariableSortOn { + fn from(mcp: crate::types::VariableSortOn) -> Self { + match mcp { + crate::types::VariableSortOn::Name => superposition_sdk::types::VariableSortOn::Name, + crate::types::VariableSortOn::CreatedAt => superposition_sdk::types::VariableSortOn::CreatedAt, + crate::types::VariableSortOn::LastModifiedAt => superposition_sdk::types::VariableSortOn::LastModifiedAt, + } + } +} + +impl From for superposition_sdk::types::Stage { + fn from(mcp: crate::types::Stage) -> Self { + match mcp { + crate::types::Stage::Draft => superposition_sdk::types::Stage::Draft, + crate::types::Stage::Published => superposition_sdk::types::Stage::Published, + } + } +} + +impl From for superposition_sdk::types::OrgStatus { + fn from(mcp: crate::types::OrgStatus) -> Self { + match mcp { + crate::types::OrgStatus::Active => superposition_sdk::types::OrgStatus::Active, + crate::types::OrgStatus::Inactive => superposition_sdk::types::OrgStatus::Inactive, + crate::types::OrgStatus::PendingKyb => superposition_sdk::types::OrgStatus::PendingKyb, + } + } +} + +impl From for crate::types::GroupType { + fn from(sdk: superposition_sdk::types::GroupType) -> Self { + match sdk { + superposition_sdk::types::GroupType::UserCreated => crate::types::GroupType::UserCreated, + superposition_sdk::types::GroupType::SystemGenerated => crate::types::GroupType::SystemGenerated, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::GroupType::UserCreated, + } + } +} + +impl From for crate::types::VariantType { + fn from(sdk: superposition_sdk::types::VariantType) -> Self { + match sdk { + superposition_sdk::types::VariantType::Control => crate::types::VariantType::Control, + superposition_sdk::types::VariantType::Experimental => crate::types::VariantType::Experimental, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::VariantType::Control, + } + } +} + +impl From for crate::types::ExperimentType { + fn from(sdk: superposition_sdk::types::ExperimentType) -> Self { + match sdk { + superposition_sdk::types::ExperimentType::Default => crate::types::ExperimentType::Default, + superposition_sdk::types::ExperimentType::DeleteOverrides => crate::types::ExperimentType::DeleteOverrides, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::ExperimentType::Default, + } + } +} + +impl From for crate::types::ExperimentStatusType { + fn from(sdk: superposition_sdk::types::ExperimentStatusType) -> Self { + match sdk { + superposition_sdk::types::ExperimentStatusType::Created => crate::types::ExperimentStatusType::Created, + superposition_sdk::types::ExperimentStatusType::Concluded => crate::types::ExperimentStatusType::Concluded, + superposition_sdk::types::ExperimentStatusType::Inprogress => crate::types::ExperimentStatusType::Inprogress, + superposition_sdk::types::ExperimentStatusType::Discarded => crate::types::ExperimentStatusType::Discarded, + superposition_sdk::types::ExperimentStatusType::Paused => crate::types::ExperimentStatusType::Paused, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::ExperimentStatusType::Created, + } + } +} + +impl From for crate::types::FunctionRuntimeVersion { + fn from(sdk: superposition_sdk::types::FunctionRuntimeVersion) -> Self { + match sdk { + superposition_sdk::types::FunctionRuntimeVersion::V1 => crate::types::FunctionRuntimeVersion::V1, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::FunctionRuntimeVersion::V1, + } + } +} + +impl From for crate::types::FunctionTypes { + fn from(sdk: superposition_sdk::types::FunctionTypes) -> Self { + match sdk { + superposition_sdk::types::FunctionTypes::ValueValidation => crate::types::FunctionTypes::ValueValidation, + superposition_sdk::types::FunctionTypes::ValueCompute => crate::types::FunctionTypes::ValueCompute, + superposition_sdk::types::FunctionTypes::ContextValidation => crate::types::FunctionTypes::ContextValidation, + superposition_sdk::types::FunctionTypes::ChangeReasonValidation => crate::types::FunctionTypes::ChangeReasonValidation, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::FunctionTypes::ValueValidation, + } + } +} + +impl From for crate::types::OrgStatus { + fn from(sdk: superposition_sdk::types::OrgStatus) -> Self { + match sdk { + superposition_sdk::types::OrgStatus::Active => crate::types::OrgStatus::Active, + superposition_sdk::types::OrgStatus::Inactive => crate::types::OrgStatus::Inactive, + superposition_sdk::types::OrgStatus::PendingKyb => crate::types::OrgStatus::PendingKyb, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::OrgStatus::Active, + } + } +} + +impl From for crate::types::HttpMethod { + fn from(sdk: superposition_sdk::types::HttpMethod) -> Self { + match sdk { + superposition_sdk::types::HttpMethod::Get => crate::types::HttpMethod::Get, + superposition_sdk::types::HttpMethod::Post => crate::types::HttpMethod::Post, + superposition_sdk::types::HttpMethod::Put => crate::types::HttpMethod::Put, + superposition_sdk::types::HttpMethod::Patch => crate::types::HttpMethod::Patch, + superposition_sdk::types::HttpMethod::Delete => crate::types::HttpMethod::Delete, + superposition_sdk::types::HttpMethod::Head => crate::types::HttpMethod::Head, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::HttpMethod::Get, + } + } +} + +impl From for crate::types::Version { + fn from(sdk: superposition_sdk::types::Version) -> Self { + match sdk { + superposition_sdk::types::Version::V1 => crate::types::Version::V1, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::Version::V1, + } + } +} + +impl From for crate::types::WorkspaceStatus { + fn from(sdk: superposition_sdk::types::WorkspaceStatus) -> Self { + match sdk { + superposition_sdk::types::WorkspaceStatus::Enabled => crate::types::WorkspaceStatus::Enabled, + superposition_sdk::types::WorkspaceStatus::Disabled => crate::types::WorkspaceStatus::Disabled, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::WorkspaceStatus::Enabled, + } + } +} + +impl From for crate::types::AuditAction { + fn from(sdk: superposition_sdk::types::AuditAction) -> Self { + match sdk { + superposition_sdk::types::AuditAction::Insert => crate::types::AuditAction::Insert, + superposition_sdk::types::AuditAction::Update => crate::types::AuditAction::Update, + superposition_sdk::types::AuditAction::Delete => crate::types::AuditAction::Delete, + // SDK open-enum fallback (Unknown variant): degrade to the first known variant. + _ => crate::types::AuditAction::Insert, + } + } +} + +impl From for superposition_sdk::types::ContextAction { + fn from(mcp: crate::types::ContextAction) -> Self { + match mcp { + crate::types::ContextAction::Put(inner) => superposition_sdk::types::ContextAction::Put(inner.into()), + crate::types::ContextAction::Replace(inner) => superposition_sdk::types::ContextAction::Replace(inner.into()), + crate::types::ContextAction::Delete(inner) => superposition_sdk::types::ContextAction::Delete(inner), + crate::types::ContextAction::Move(inner) => superposition_sdk::types::ContextAction::Move(inner.into()), + } + } +} + +impl From for superposition_sdk::types::ContextIdentifier { + fn from(mcp: crate::types::ContextIdentifier) -> Self { + match mcp { + crate::types::ContextIdentifier::Id(inner) => superposition_sdk::types::ContextIdentifier::Id(inner), + crate::types::ContextIdentifier::Context(inner) => superposition_sdk::types::ContextIdentifier::Context(inner.into_iter().map(|(k, v)| (k, smithy_mcp_runtime::value_to_document(v))).collect()), + } + } +} + +impl From for superposition_sdk::types::DimensionType { + fn from(mcp: crate::types::DimensionType) -> Self { + match mcp { + crate::types::DimensionType::Regular => superposition_sdk::types::DimensionType::Regular, + crate::types::DimensionType::LocalCohort(inner) => superposition_sdk::types::DimensionType::LocalCohort(inner), + crate::types::DimensionType::RemoteCohort(inner) => superposition_sdk::types::DimensionType::RemoteCohort(inner), + } + } +} + +impl From for superposition_sdk::types::FunctionExecutionRequest { + fn from(mcp: crate::types::FunctionExecutionRequest) -> Self { + match mcp { + crate::types::FunctionExecutionRequest::ValueValidate(inner) => superposition_sdk::types::FunctionExecutionRequest::ValueValidate(inner.into()), + crate::types::FunctionExecutionRequest::ValueCompute(inner) => superposition_sdk::types::FunctionExecutionRequest::ValueCompute(inner.into()), + crate::types::FunctionExecutionRequest::ContextValidate(inner) => superposition_sdk::types::FunctionExecutionRequest::ContextValidate(inner.into()), + crate::types::FunctionExecutionRequest::ChangeReasonValidate(inner) => superposition_sdk::types::FunctionExecutionRequest::ChangeReasonValidate(inner.into()), + } + } +} + +impl From for crate::types::ContextActionOut { + fn from(sdk: superposition_sdk::types::ContextActionOut) -> Self { + match sdk { + superposition_sdk::types::ContextActionOut::Put(inner) => crate::types::ContextActionOut::Put(inner.into()), + superposition_sdk::types::ContextActionOut::Replace(inner) => crate::types::ContextActionOut::Replace(inner.into()), + superposition_sdk::types::ContextActionOut::Delete(inner) => crate::types::ContextActionOut::Delete(inner), + superposition_sdk::types::ContextActionOut::Move(inner) => crate::types::ContextActionOut::Move(inner.into()), + // smithy-rs unions are `#[non_exhaustive]` with an `Unknown` arm — + // it's only delivered when the server adds a variant the client + // doesn't recognise. Panic loudly rather than silently corrupting data. + _ => panic!("received unknown variant for SDK union ContextActionOut — SDK is out of date"), + } + } +} + +impl From for crate::types::DimensionType { + fn from(sdk: superposition_sdk::types::DimensionType) -> Self { + match sdk { + superposition_sdk::types::DimensionType::Regular => crate::types::DimensionType::Regular, + superposition_sdk::types::DimensionType::LocalCohort(inner) => crate::types::DimensionType::LocalCohort(inner), + superposition_sdk::types::DimensionType::RemoteCohort(inner) => crate::types::DimensionType::RemoteCohort(inner), + // smithy-rs unions are `#[non_exhaustive]` with an `Unknown` arm — + // it's only delivered when the server adds a variant the client + // doesn't recognise. Panic loudly rather than silently corrupting data. + _ => panic!("received unknown variant for SDK union DimensionType — SDK is out of date"), + } + } +} + diff --git a/crates/superposition_mcp/src/lib.rs b/crates/superposition_mcp/src/lib.rs new file mode 100644 index 000000000..ba9318af8 --- /dev/null +++ b/crates/superposition_mcp/src/lib.rs @@ -0,0 +1,7 @@ +pub mod types; +pub mod tools; +pub mod server; +pub mod conversions; + +pub use types::*; +pub use server::McpServer; diff --git a/crates/superposition_mcp/src/server.rs b/crates/superposition_mcp/src/server.rs new file mode 100644 index 000000000..0863cae93 --- /dev/null +++ b/crates/superposition_mcp/src/server.rs @@ -0,0 +1,535 @@ +use std::sync::Arc; +use smithy_mcp_runtime::{McpError, Router}; +use crate::tools; +use crate::types::*; + +pub struct McpServer { + router: Router, +} + +impl McpServer { + pub fn new(client: superposition_sdk::Client) -> Self { + let client = Arc::new(client); + let mut router = Router::new(); + + let c = client.clone(); + router.register_tool(tools::tool_info_add_members_to_group(), move |params| { + let client = c.clone(); + async move { tools::handle_add_members_to_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_applicable_variants(), move |params| { + let client = c.clone(); + async move { tools::handle_applicable_variants(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_bulk_operation(), move |params| { + let client = c.clone(); + async move { tools::handle_bulk_operation(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_conclude_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_conclude_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_context(), move |params| { + let client = c.clone(); + async move { tools::handle_create_context(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_default_config(), move |params| { + let client = c.clone(); + async move { tools::handle_create_default_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_dimension(), move |params| { + let client = c.clone(); + async move { tools::handle_create_dimension(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_create_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_experiment_group(), move |params| { + let client = c.clone(); + async move { tools::handle_create_experiment_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_function(), move |params| { + let client = c.clone(); + async move { tools::handle_create_function(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_organisation(), move |params| { + let client = c.clone(); + async move { tools::handle_create_organisation(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_secret(), move |params| { + let client = c.clone(); + async move { tools::handle_create_secret(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_type_templates(), move |params| { + let client = c.clone(); + async move { tools::handle_create_type_templates(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_variable(), move |params| { + let client = c.clone(); + async move { tools::handle_create_variable(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_webhook(), move |params| { + let client = c.clone(); + async move { tools::handle_create_webhook(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_create_workspace(), move |params| { + let client = c.clone(); + async move { tools::handle_create_workspace(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_context(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_context(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_default_config(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_default_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_dimension(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_dimension(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_experiment_group(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_experiment_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_function(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_function(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_secret(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_secret(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_type_templates(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_type_templates(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_variable(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_variable(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_delete_webhook(), move |params| { + let client = c.clone(); + async move { tools::handle_delete_webhook(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_discard_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_discard_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_config(), move |params| { + let client = c.clone(); + async move { tools::handle_get_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_config_json(), move |params| { + let client = c.clone(); + async move { tools::handle_get_config_json(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_config_toml(), move |params| { + let client = c.clone(); + async move { tools::handle_get_config_toml(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_context(), move |params| { + let client = c.clone(); + async move { tools::handle_get_context(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_context_from_condition(), move |params| { + let client = c.clone(); + async move { tools::handle_get_context_from_condition(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_default_config(), move |params| { + let client = c.clone(); + async move { tools::handle_get_default_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_dimension(), move |params| { + let client = c.clone(); + async move { tools::handle_get_dimension(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_get_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_experiment_config(), move |params| { + let client = c.clone(); + async move { tools::handle_get_experiment_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_experiment_group(), move |params| { + let client = c.clone(); + async move { tools::handle_get_experiment_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_function(), move |params| { + let client = c.clone(); + async move { tools::handle_get_function(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_organisation(), move |params| { + let client = c.clone(); + async move { tools::handle_get_organisation(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_resolved_config(), move |params| { + let client = c.clone(); + async move { tools::handle_get_resolved_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_resolved_config_with_identifier(), move |params| { + let client = c.clone(); + async move { tools::handle_get_resolved_config_with_identifier(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_secret(), move |params| { + let client = c.clone(); + async move { tools::handle_get_secret(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_type_template(), move |params| { + let client = c.clone(); + async move { tools::handle_get_type_template(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_type_templates_list(), move |params| { + let client = c.clone(); + async move { tools::handle_get_type_templates_list(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_variable(), move |params| { + let client = c.clone(); + async move { tools::handle_get_variable(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_version(), move |params| { + let client = c.clone(); + async move { tools::handle_get_version(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_webhook(), move |params| { + let client = c.clone(); + async move { tools::handle_get_webhook(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_webhook_by_event(), move |params| { + let client = c.clone(); + async move { tools::handle_get_webhook_by_event(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_get_workspace(), move |params| { + let client = c.clone(); + async move { tools::handle_get_workspace(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_audit_logs(), move |params| { + let client = c.clone(); + async move { tools::handle_list_audit_logs(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_contexts(), move |params| { + let client = c.clone(); + async move { tools::handle_list_contexts(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_default_configs(), move |params| { + let client = c.clone(); + async move { tools::handle_list_default_configs(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_dimensions(), move |params| { + let client = c.clone(); + async move { tools::handle_list_dimensions(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_list_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_experiment_groups(), move |params| { + let client = c.clone(); + async move { tools::handle_list_experiment_groups(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_function(), move |params| { + let client = c.clone(); + async move { tools::handle_list_function(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_organisation(), move |params| { + let client = c.clone(); + async move { tools::handle_list_organisation(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_secrets(), move |params| { + let client = c.clone(); + async move { tools::handle_list_secrets(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_variables(), move |params| { + let client = c.clone(); + async move { tools::handle_list_variables(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_versions(), move |params| { + let client = c.clone(); + async move { tools::handle_list_versions(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_webhook(), move |params| { + let client = c.clone(); + async move { tools::handle_list_webhook(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_list_workspace(), move |params| { + let client = c.clone(); + async move { tools::handle_list_workspace(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_migrate_workspace_schema(), move |params| { + let client = c.clone(); + async move { tools::handle_migrate_workspace_schema(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_move_context(), move |params| { + let client = c.clone(); + async move { tools::handle_move_context(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_pause_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_pause_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_publish(), move |params| { + let client = c.clone(); + async move { tools::handle_publish(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_ramp_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_ramp_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_remove_members_from_group(), move |params| { + let client = c.clone(); + async move { tools::handle_remove_members_from_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_resume_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_resume_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_rotate_master_encryption_key(), move |params| { + let client = c.clone(); + async move { tools::handle_rotate_master_encryption_key(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_rotate_workspace_encryption_key(), move |params| { + let client = c.clone(); + async move { tools::handle_rotate_workspace_encryption_key(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_test(), move |params| { + let client = c.clone(); + async move { tools::handle_test(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_default_config(), move |params| { + let client = c.clone(); + async move { tools::handle_update_default_config(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_dimension(), move |params| { + let client = c.clone(); + async move { tools::handle_update_dimension(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_experiment_group(), move |params| { + let client = c.clone(); + async move { tools::handle_update_experiment_group(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_function(), move |params| { + let client = c.clone(); + async move { tools::handle_update_function(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_organisation(), move |params| { + let client = c.clone(); + async move { tools::handle_update_organisation(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_override(), move |params| { + let client = c.clone(); + async move { tools::handle_update_override(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_overrides_experiment(), move |params| { + let client = c.clone(); + async move { tools::handle_update_overrides_experiment(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_secret(), move |params| { + let client = c.clone(); + async move { tools::handle_update_secret(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_type_templates(), move |params| { + let client = c.clone(); + async move { tools::handle_update_type_templates(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_variable(), move |params| { + let client = c.clone(); + async move { tools::handle_update_variable(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_webhook(), move |params| { + let client = c.clone(); + async move { tools::handle_update_webhook(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_update_workspace(), move |params| { + let client = c.clone(); + async move { tools::handle_update_workspace(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_validate_context(), move |params| { + let client = c.clone(); + async move { tools::handle_validate_context(&client, params).await } + }); + + let c = client.clone(); + router.register_tool(tools::tool_info_weight_recompute(), move |params| { + let client = c.clone(); + async move { tools::handle_weight_recompute(&client, params).await } + }); + + Self { router } + } + + pub async fn serve_stdio(self) -> Result<(), Box> { + smithy_mcp_runtime::serve_stdio(self.router).await + } + + pub fn into_router(self) -> Router { + self.router + } +} diff --git a/crates/superposition_mcp/src/tools.rs b/crates/superposition_mcp/src/tools.rs new file mode 100644 index 000000000..d4eca0043 --- /dev/null +++ b/crates/superposition_mcp/src/tools.rs @@ -0,0 +1,2852 @@ +use smithy_mcp_runtime::{McpError, ToolInfo}; +use crate::types::*; +use superposition_sdk::Client; + +pub async fn handle_add_members_to_group(client: &Client, params: serde_json::Value) -> Result { + let input: ModifyMembersToGroupRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::add_members_to_group::builders::AddMembersToGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_add_members_to_group() -> ToolInfo { + ToolInfo { + name: "AddMembersToGroup".to_string(), + description: "Adds members to an existing experiment group.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" }, + "member_experiment_ids": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["workspace_id", "org_id", "id", "change_reason", "member_experiment_ids"] +}), + } +} + +pub async fn handle_applicable_variants(client: &Client, params: serde_json::Value) -> Result { + let input: ApplicableVariantsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::applicable_variants::builders::ApplicableVariantsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ApplicableVariantsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_applicable_variants() -> ToolInfo { + ToolInfo { + name: "ApplicableVariants".to_string(), + description: "Determines which experiment variants are applicable to a given context, used for experiment evaluation and variant selection.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "identifier": { "type": "string" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["workspace_id", "org_id", "context", "identifier"] +}), + } +} + +pub async fn handle_bulk_operation(client: &Client, params: serde_json::Value) -> Result { + let input: BulkOperationInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::bulk_operation::builders::BulkOperationInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: BulkOperationOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_bulk_operation() -> ToolInfo { + ToolInfo { + name: "BulkOperation".to_string(), + description: "Executes multiple context operations (PUT, REPLACE, DELETE, MOVE) in a single atomic transaction for efficient batch processing.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "config_tags": { "type": "string" }, + "operations": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "PUT": { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "override": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "override", "change_reason"] + } + }, + "required": ["PUT"] + }, + { + "type": "object", + "properties": { + "REPLACE": { + "type": "object", + "properties": { + "context": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" } + }, + "required": ["id"] + }, + { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["context"] + } + ] + }, + "override": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "override", "change_reason"] + } + }, + "required": ["REPLACE"] + }, + { + "type": "object", + "properties": { + "DELETE": { "type": "string" } + }, + "required": ["DELETE"] + }, + { + "type": "object", + "properties": { + "MOVE": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "request": { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "change_reason"] + } + }, + "required": ["id", "request"] + } + }, + "required": ["MOVE"] + } + ] + } + } + }, + "required": ["workspace_id", "org_id", "operations"] +}), + } +} + +pub async fn handle_conclude_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: ConcludeExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::conclude_experiment::builders::ConcludeExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_conclude_experiment() -> ToolInfo { + ToolInfo { + name: "ConcludeExperiment".to_string(), + description: "Concludes an inprogress experiment by selecting a winning variant and transitioning the experiment to a concluded state.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "chosen_variant": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id", "chosen_variant", "change_reason"] +}), + } +} + +pub async fn handle_create_context(client: &Client, params: serde_json::Value) -> Result { + let input: CreateContextInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_context::builders::CreateContextInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ContextResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_context() -> ToolInfo { + ToolInfo { + name: "CreateContext".to_string(), + description: "Creates a new context with specified conditions and overrides. Contexts define conditional rules for config management.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "config_tags": { "type": "string" }, + "request": { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "override": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "override", "change_reason"] + } + }, + "required": ["workspace_id", "org_id", "request"] +}), + } +} + +pub async fn handle_create_default_config(client: &Client, params: serde_json::Value) -> Result { + let input: CreateDefaultConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_default_config::builders::CreateDefaultConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DefaultConfigResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_default_config() -> ToolInfo { + ToolInfo { + name: "CreateDefaultConfig".to_string(), + description: "Creates a new default config entry with specified key, value, schema, and metadata. Default configs serve as fallback values when no specific context matches.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "object" }, + "schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "value_validation_function_name": { "type": "string" }, + "value_compute_function_name": { "type": "string" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" } + }, + "required": ["key", "value", "schema", "description", "change_reason", "workspace_id", "org_id"] +}), + } +} + +pub async fn handle_create_dimension(client: &Client, params: serde_json::Value) -> Result { + let input: CreateDimensionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_dimension::builders::CreateDimensionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DimensionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_dimension() -> ToolInfo { + ToolInfo { + name: "CreateDimension".to_string(), + description: "Creates a new dimension with the specified json schema. Dimensions define categorical attributes used for context-based config management.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "dimension": { "type": "string" }, + "position": { "type": "integer" }, + "schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "value_validation_function_name": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "dimension_type": { + "oneOf": [ + { + "type": "object", + "properties": { + "REGULAR": { "type": "null" } + }, + "required": ["REGULAR"] + }, + { + "type": "object", + "properties": { + "LOCAL_COHORT": { "type": "string" } + }, + "required": ["LOCAL_COHORT"] + }, + { + "type": "object", + "properties": { + "REMOTE_COHORT": { "type": "string" } + }, + "required": ["REMOTE_COHORT"] + } + ] + }, + "value_compute_function_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "dimension", "position", "schema", "description", "change_reason"] +}), + } +} + +pub async fn handle_create_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: CreateExperimentRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_experiment::builders::CreateExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_experiment() -> ToolInfo { + ToolInfo { + name: "CreateExperiment".to_string(), + description: "Creates a new experiment with variants, context and conditions. You can optionally specify metrics and experiment group for tracking and analysis.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "experiment_type": { + "type": "string", + "enum": ["DEFAULT", "DELETE_OVERRIDES"] + }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "variant_type": { + "type": "string", + "enum": ["CONTROL", "EXPERIMENTAL"] + }, + "context_id": { "type": "string" }, + "override_id": { "type": "string" }, + "overrides": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["id", "variant_type", "overrides"] + } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "metrics": { "type": "object" }, + "experiment_group_id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "context", "variants", "description", "change_reason"] +}), + } +} + +pub async fn handle_create_experiment_group(client: &Client, params: serde_json::Value) -> Result { + let input: CreateExperimentGroupRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_experiment_group::builders::CreateExperimentGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_experiment_group() -> ToolInfo { + ToolInfo { + name: "CreateExperimentGroup".to_string(), + description: "Creates a new experiment group.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "traffic_percentage": { "type": "integer" }, + "member_experiment_ids": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["workspace_id", "org_id", "name", "description", "change_reason", "context", "traffic_percentage"] +}), + } +} + +pub async fn handle_create_function(client: &Client, params: serde_json::Value) -> Result { + let input: CreateFunctionRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_function::builders::CreateFunctionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: FunctionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_function() -> ToolInfo { + ToolInfo { + name: "CreateFunction".to_string(), + description: "Creates a new custom function for value_validation, value_compute, context_validation or change_reason_validation with specified code, runtime version, and function type.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "function": { "type": "string" }, + "runtime_version": { + "type": "string", + "enum": ["1.0"] + }, + "function_type": { + "type": "string", + "enum": ["VALUE_VALIDATION", "VALUE_COMPUTE", "CONTEXT_VALIDATION", "CHANGE_REASON_VALIDATION"] + } + }, + "required": ["workspace_id", "org_id", "function_name", "description", "change_reason", "function", "runtime_version", "function_type"] +}), + } +} + +pub async fn handle_create_organisation(client: &Client, params: serde_json::Value) -> Result { + let input: CreateOrganisationRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_organisation::builders::CreateOrganisationInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: OrganisationResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_organisation() -> ToolInfo { + ToolInfo { + name: "CreateOrganisation".to_string(), + description: "Creates a new organisation with specified name and administrator email. This is the top-level entity that contains workspaces and manages organizational-level settings.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "country_code": { "type": "string" }, + "contact_email": { "type": "string" }, + "contact_phone": { "type": "string" }, + "admin_email": { "type": "string" }, + "sector": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["admin_email", "name"] +}), + } +} + +pub async fn handle_create_secret(client: &Client, params: serde_json::Value) -> Result { + let input: CreateSecretInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_secret::builders::CreateSecretInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: SecretResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_secret() -> ToolInfo { + ToolInfo { + name: "CreateSecret".to_string(), + description: "Creates a new encrypted secret with the specified name and value. The secret is encrypted with the workspace's current encryption key. Secret values are never returned in responses for security.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "value": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "value", "description", "change_reason"] +}), + } +} + +pub async fn handle_create_type_templates(client: &Client, params: serde_json::Value) -> Result { + let input: CreateTypeTemplatesRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_type_templates::builders::CreateTypeTemplatesInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: TypeTemplatesResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_type_templates() -> ToolInfo { + ToolInfo { + name: "CreateTypeTemplates".to_string(), + description: "Creates a new type template with specified schema definition, providing reusable type definitions for config validation.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "type_name": { "type": "string" }, + "type_schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "type_name", "type_schema", "description", "change_reason"] +}), + } +} + +pub async fn handle_create_variable(client: &Client, params: serde_json::Value) -> Result { + let input: CreateVariableInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_variable::builders::CreateVariableInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: VariableResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_variable() -> ToolInfo { + ToolInfo { + name: "CreateVariable".to_string(), + description: "Creates a new variable with the specified name and value.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "value": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "value", "description", "change_reason"] +}), + } +} + +pub async fn handle_create_webhook(client: &Client, params: serde_json::Value) -> Result { + let input: CreateWebhookInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_webhook::builders::CreateWebhookInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WebhookResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_webhook() -> ToolInfo { + ToolInfo { + name: "CreateWebhook".to_string(), + description: "Creates a new webhook config to receive HTTP notifications when specified events occur in the system.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "description": { "type": "string" }, + "enabled": { "type": "boolean" }, + "url": { "type": "string" }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] + }, + "version": { + "type": "string", + "enum": ["V1"] + }, + "custom_headers": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "events": { + "type": "array", + "items": { "type": "string" } + }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "description", "enabled", "url", "method", "events", "change_reason"] +}), + } +} + +pub async fn handle_create_workspace(client: &Client, params: serde_json::Value) -> Result { + let input: CreateWorkspaceRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::create_workspace::builders::CreateWorkspaceInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WorkspaceResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_create_workspace() -> ToolInfo { + ToolInfo { + name: "CreateWorkspace".to_string(), + description: "Creates a new workspace within an organisation, including database schema setup and isolated environment for config management with specified admin and settings.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "org_id": { "type": "string" }, + "workspace_admin_email": { "type": "string" }, + "workspace_name": { "type": "string" }, + "workspace_status": { + "type": "string", + "enum": ["ENABLED", "DISABLED"] + }, + "metrics": { "type": "object" }, + "allow_experiment_self_approval": { "type": "boolean" }, + "auto_populate_control": { "type": "boolean" }, + "enable_context_validation": { "type": "boolean" }, + "enable_change_reason_validation": { "type": "boolean" } + }, + "required": ["org_id", "workspace_admin_email", "workspace_name"] +}), + } +} + +pub async fn handle_delete_context(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteContextInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_context::builders::DeleteContextInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_context() -> ToolInfo { + ToolInfo { + name: "DeleteContext".to_string(), + description: "Permanently removes a context from the workspace. This operation cannot be undone and will affect config resolution.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "config_tags": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_delete_default_config(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteDefaultConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_default_config::builders::DeleteDefaultConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_default_config() -> ToolInfo { + ToolInfo { + name: "DeleteDefaultConfig".to_string(), + description: "Permanently removes a default config entry from the workspace. This operation cannot be performed if it affects config resolution for contexts that rely on this fallback value.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "key": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "key"] +}), + } +} + +pub async fn handle_delete_dimension(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteDimensionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_dimension::builders::DeleteDimensionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_dimension() -> ToolInfo { + ToolInfo { + name: "DeleteDimension".to_string(), + description: "Permanently removes a dimension from the workspace. This operation will fail if the dimension has active dependencies or is referenced by existing configurations.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "dimension": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "dimension"] +}), + } +} + +pub async fn handle_delete_experiment_group(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteExperimentGroupInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_experiment_group::builders::DeleteExperimentGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_experiment_group() -> ToolInfo { + ToolInfo { + name: "DeleteExperimentGroup".to_string(), + description: "Deletes an experiment group.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_delete_function(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteFunctionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_function::builders::DeleteFunctionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_function() -> ToolInfo { + ToolInfo { + name: "DeleteFunction".to_string(), + description: "Permanently removes a function from the workspace, deleting both draft and published versions along with all associated code. It fails if already in use".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "function_name"] +}), + } +} + +pub async fn handle_delete_secret(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteSecretInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_secret::builders::DeleteSecretInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: SecretResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_secret() -> ToolInfo { + ToolInfo { + name: "DeleteSecret".to_string(), + description: "Permanently deletes a secret from the workspace. The encrypted value is removed and cannot be recovered.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_delete_type_templates(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteTypeTemplatesInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_type_templates::builders::DeleteTypeTemplatesInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: TypeTemplatesResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_type_templates() -> ToolInfo { + ToolInfo { + name: "DeleteTypeTemplates".to_string(), + description: "Permanently removes a type template from the workspace. No checks performed while deleting".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "type_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "type_name"] +}), + } +} + +pub async fn handle_delete_variable(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteVariableInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_variable::builders::DeleteVariableInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: VariableResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_variable() -> ToolInfo { + ToolInfo { + name: "DeleteVariable".to_string(), + description: "Permanently deletes a variable from the workspace.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_delete_webhook(client: &Client, params: serde_json::Value) -> Result { + let input: DeleteWebhookInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::delete_webhook::builders::DeleteWebhookInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_delete_webhook() -> ToolInfo { + ToolInfo { + name: "DeleteWebhook".to_string(), + description: "Permanently removes a webhook config from the workspace, stopping all future event notifications to that endpoint.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_discard_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: DiscardExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::discard_experiment::builders::DiscardExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_discard_experiment() -> ToolInfo { + ToolInfo { + name: "DiscardExperiment".to_string(), + description: "Discards an experiment without selecting a winner, effectively canceling the experiment and removing its effects.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id", "change_reason"] +}), + } +} + +pub async fn handle_get_config(client: &Client, params: serde_json::Value) -> Result { + let input: GetConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_config::builders::GetConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetConfigOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_config() -> ToolInfo { + ToolInfo { + name: "GetConfig".to_string(), + description: "Retrieves config data with context evaluation, including applicable contexts, overrides, and default values based on provided conditions.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "version": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_config_json(client: &Client, params: serde_json::Value) -> Result { + let input: GetConfigJsonInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_config_json::builders::GetConfigJsonInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetConfigJsonOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_config_json() -> ToolInfo { + ToolInfo { + name: "GetConfigJson".to_string(), + description: "Retrieves the full config in JSON format, including default configs with schemas, dimensions, and overrides. This endpoint is optimized for clients that prefer JSON format for configuration management.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_config_toml(client: &Client, params: serde_json::Value) -> Result { + let input: GetConfigTomlInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_config_toml::builders::GetConfigTomlInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetConfigTomlOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_config_toml() -> ToolInfo { + ToolInfo { + name: "GetConfigToml".to_string(), + description: "Retrieves the full config in TOML format, including default configs with schemas, dimensions, and overrides. This endpoint is optimized for clients that prefer TOML format for configuration management.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_context(client: &Client, params: serde_json::Value) -> Result { + let input: GetContextInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_context::builders::GetContextInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ContextResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_context() -> ToolInfo { + ToolInfo { + name: "GetContext".to_string(), + description: "Retrieves detailed information about a specific context by its unique identifier, including conditions, overrides, and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_get_context_from_condition(client: &Client, params: serde_json::Value) -> Result { + let input: GetContextFromConditionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_context_from_condition::builders::GetContextFromConditionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ContextResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_context_from_condition() -> ToolInfo { + ToolInfo { + name: "GetContextFromCondition".to_string(), + description: "Retrieves context information by matching against provided conditions. Used to find contexts that would apply to specific scenarios.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "context": { "type": "object" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_default_config(client: &Client, params: serde_json::Value) -> Result { + let input: GetDefaultConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_default_config::builders::GetDefaultConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DefaultConfigResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_default_config() -> ToolInfo { + ToolInfo { + name: "GetDefaultConfig".to_string(), + description: "Retrieves a specific default config entry by its key, including its value, schema, function mappings, and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "key": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "key"] +}), + } +} + +pub async fn handle_get_dimension(client: &Client, params: serde_json::Value) -> Result { + let input: GetDimensionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_dimension::builders::GetDimensionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DimensionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_dimension() -> ToolInfo { + ToolInfo { + name: "GetDimension".to_string(), + description: "Retrieves detailed information about a specific dimension, including its schema, cohort dependency graph, and configuration metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "dimension": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "dimension"] +}), + } +} + +pub async fn handle_get_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: GetExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_experiment::builders::GetExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_experiment() -> ToolInfo { + ToolInfo { + name: "GetExperiment".to_string(), + description: "Retrieves detailed information about a specific experiment, including its config, variants, status, and metrics.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_get_experiment_config(client: &Client, params: serde_json::Value) -> Result { + let input: GetExperimentConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_experiment_config::builders::GetExperimentConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetExperimentConfigOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_experiment_config() -> ToolInfo { + ToolInfo { + name: "GetExperimentConfig".to_string(), + description: "Retrieves the experiment configuration for a given workspace and organization. The response includes details of all experiment groups and experiments that match the specified filters.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "dimension_match_strategy": { + "type": "string", + "enum": ["exact", "subset"] + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_experiment_group(client: &Client, params: serde_json::Value) -> Result { + let input: GetExperimentGroupInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_experiment_group::builders::GetExperimentGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_experiment_group() -> ToolInfo { + ToolInfo { + name: "GetExperimentGroup".to_string(), + description: "Retrieves an existing experiment group by its ID.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_get_function(client: &Client, params: serde_json::Value) -> Result { + let input: GetFunctionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_function::builders::GetFunctionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: FunctionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_function() -> ToolInfo { + ToolInfo { + name: "GetFunction".to_string(), + description: "Retrieves detailed information about a specific function including its published and draft versions, code, and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "function_name"] +}), + } +} + +pub async fn handle_get_organisation(client: &Client, params: serde_json::Value) -> Result { + let input: GetOrganisationInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_organisation::builders::GetOrganisationInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: OrganisationResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_organisation() -> ToolInfo { + ToolInfo { + name: "GetOrganisation".to_string(), + description: "Retrieves detailed information about a specific organisation including its status, contact details, and administrative metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "id": { "type": "string" } + }, + "required": ["id"] +}), + } +} + +pub async fn handle_get_resolved_config(client: &Client, params: serde_json::Value) -> Result { + let input: GetResolvedConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_resolved_config::builders::GetResolvedConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetResolvedConfigOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_resolved_config() -> ToolInfo { + ToolInfo { + name: "GetResolvedConfig".to_string(), + description: "Resolves and merges config values based on context conditions, applying overrides and merge strategies to produce the final configuration.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "version": { "type": "string" }, + "show_reasoning": { "type": "boolean" }, + "merge_strategy": { + "type": "string", + "enum": ["MERGE", "REPLACE"] + }, + "context_id": { "type": "string" }, + "resolve_remote": { "type": "boolean" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_resolved_config_with_identifier(client: &Client, params: serde_json::Value) -> Result { + let input: GetResolvedConfigWithIdentifierInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_resolved_config_with_identifier::builders::GetResolvedConfigWithIdentifierInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetResolvedConfigWithIdentifierOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_resolved_config_with_identifier() -> ToolInfo { + ToolInfo { + name: "GetResolvedConfigWithIdentifier".to_string(), + description: "Resolves and merges config values based on context conditions and identifier, applying overrides and merge strategies to produce the final configuration.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "version": { "type": "string" }, + "show_reasoning": { "type": "boolean" }, + "merge_strategy": { + "type": "string", + "enum": ["MERGE", "REPLACE"] + }, + "context_id": { "type": "string" }, + "resolve_remote": { "type": "boolean" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "identifier": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_secret(client: &Client, params: serde_json::Value) -> Result { + let input: GetSecretInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_secret::builders::GetSecretInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: SecretResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_secret() -> ToolInfo { + ToolInfo { + name: "GetSecret".to_string(), + description: "Retrieves detailed information about a specific secret by its name. The value is masked for security.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_get_type_template(client: &Client, params: serde_json::Value) -> Result { + let input: GetTypeTemplateInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_type_template::builders::GetTypeTemplateInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: TypeTemplatesResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_type_template() -> ToolInfo { + ToolInfo { + name: "GetTypeTemplate".to_string(), + description: "Retrieves detailed information about a specific type template including its schema and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "type_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "type_name"] +}), + } +} + +pub async fn handle_get_type_templates_list(client: &Client, params: serde_json::Value) -> Result { + let input: GetTypeTemplatesListInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_type_templates_list::builders::GetTypeTemplatesListInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetTypeTemplatesListOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_type_templates_list() -> ToolInfo { + ToolInfo { + name: "GetTypeTemplatesList".to_string(), + description: "Retrieves a paginated list of all type templates in the workspace, including their schemas and metadata for type management.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_get_variable(client: &Client, params: serde_json::Value) -> Result { + let input: GetVariableInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_variable::builders::GetVariableInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: VariableResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_variable() -> ToolInfo { + ToolInfo { + name: "GetVariable".to_string(), + description: "Retrieves detailed information about a specific variable by its name.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_get_version(client: &Client, params: serde_json::Value) -> Result { + let input: GetVersionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_version::builders::GetVersionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: GetVersionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_version() -> ToolInfo { + ToolInfo { + name: "GetVersion".to_string(), + description: "Retrieves a specific config version along with its metadata for audit and rollback purposes.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id"] +}), + } +} + +pub async fn handle_get_webhook(client: &Client, params: serde_json::Value) -> Result { + let input: GetWebhookInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_webhook::builders::GetWebhookInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WebhookResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_webhook() -> ToolInfo { + ToolInfo { + name: "GetWebhook".to_string(), + description: "Retrieves detailed information about a specific webhook config, including its events, headers, and trigger history.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name"] +}), + } +} + +pub async fn handle_get_webhook_by_event(client: &Client, params: serde_json::Value) -> Result { + let input: GetWebhookByEventInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_webhook_by_event::builders::GetWebhookByEventInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WebhookResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_webhook_by_event() -> ToolInfo { + ToolInfo { + name: "GetWebhookByEvent".to_string(), + description: "Retrieves a webhook configuration based on a specific event type, allowing users to find which webhook is set to trigger for that event.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "event": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "event"] +}), + } +} + +pub async fn handle_get_workspace(client: &Client, params: serde_json::Value) -> Result { + let input: GetWorkspaceInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::get_workspace::builders::GetWorkspaceInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WorkspaceResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_get_workspace() -> ToolInfo { + ToolInfo { + name: "GetWorkspace".to_string(), + description: "Retrieves detailed information about a specific workspace including its configuration and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "org_id": { "type": "string" }, + "workspace_name": { "type": "string" } + }, + "required": ["org_id", "workspace_name"] +}), + } +} + +pub async fn handle_list_audit_logs(client: &Client, params: serde_json::Value) -> Result { + let input: ListAuditLogsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_audit_logs::builders::ListAuditLogsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListAuditLogsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_audit_logs() -> ToolInfo { + ToolInfo { + name: "ListAuditLogs".to_string(), + description: "Retrieves a paginated list of audit logs with support for filtering by date range, table names, actions, and usernames for compliance and monitoring purposes.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "from_date": { "type": "string", "format": "date-time" }, + "to_date": { "type": "string", "format": "date-time" }, + "tables": { + "type": "array", + "items": { "type": "string" } + }, + "action": { + "type": "array", + "items": { + "type": "string", + "enum": ["INSERT", "UPDATE", "DELETE"] + } + }, + "username": { "type": "string" }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_contexts(client: &Client, params: serde_json::Value) -> Result { + let input: ListContextsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_contexts::builders::ListContextsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListContextsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_contexts() -> ToolInfo { + ToolInfo { + name: "ListContexts".to_string(), + description: "Retrieves a paginated list of contexts with support for filtering by creation date, modification date, weight, and other criteria.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "sort_on": { + "type": "string", + "enum": ["last_modified_at", "created_at", "weight"] + }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + }, + "created_by": { + "type": "array", + "items": { "type": "string" } + }, + "last_modified_by": { + "type": "array", + "items": { "type": "string" } + }, + "plaintext": { "type": "string" }, + "dimension_match_strategy": { + "type": "string", + "enum": ["exact", "subset"] + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_default_configs(client: &Client, params: serde_json::Value) -> Result { + let input: ListDefaultConfigsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_default_configs::builders::ListDefaultConfigsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListDefaultConfigsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_default_configs() -> ToolInfo { + ToolInfo { + name: "ListDefaultConfigs".to_string(), + description: "Retrieves a paginated list of all default config entries in the workspace, including their values, schemas, and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "name": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_dimensions(client: &Client, params: serde_json::Value) -> Result { + let input: ListDimensionsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_dimensions::builders::ListDimensionsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListDimensionsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_dimensions() -> ToolInfo { + ToolInfo { + name: "ListDimensions".to_string(), + description: "Retrieves a paginated list of all dimensions in the workspace. Dimensions are returned with their details and metadata.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: ListExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_experiment::builders::ListExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListExperimentOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_experiment() -> ToolInfo { + ToolInfo { + name: "ListExperiment".to_string(), + description: "Retrieves a paginated list of experiments with support for filtering by status, date range, name, creator, and experiment group.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" }, + "status": { + "type": "array", + "items": { + "type": "string", + "enum": ["CREATED", "CONCLUDED", "INPROGRESS", "DISCARDED", "PAUSED"] + } + }, + "from_date": { "type": "string", "format": "date-time" }, + "to_date": { "type": "string", "format": "date-time" }, + "experiment_name": { "type": "string" }, + "experiment_ids": { + "type": "array", + "items": { "type": "string" } + }, + "experiment_group_ids": { + "type": "array", + "items": { "type": "string" } + }, + "created_by": { + "type": "array", + "items": { "type": "string" } + }, + "sort_on": { + "type": "string", + "enum": ["last_modified_at", "created_at"] + }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + }, + "global_experiments_only": { "type": "boolean" }, + "dimension_match_strategy": { + "type": "string", + "enum": ["exact", "subset"] + }, + "prefix": { + "type": "array", + "items": { "type": "string" } + }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_experiment_groups(client: &Client, params: serde_json::Value) -> Result { + let input: ListExperimentGroupsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_experiment_groups::builders::ListExperimentGroupsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListExperimentGroupsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_experiment_groups() -> ToolInfo { + ToolInfo { + name: "ListExperimentGroups".to_string(), + description: "Lists experiment groups, with support for filtering and pagination.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "if_modified_since": { "type": "string", "format": "date-time" }, + "name": { "type": "string" }, + "created_by": { "type": "string" }, + "last_modified_by": { "type": "string" }, + "sort_on": { + "type": "string", + "enum": ["name", "created_at", "last_modified_at"] + }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + }, + "group_type": { + "type": "array", + "items": { + "type": "string", + "enum": ["USER_CREATED", "SYSTEM_GENERATED"] + } + }, + "dimension_match_strategy": { + "type": "string", + "enum": ["exact", "subset"] + }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_function(client: &Client, params: serde_json::Value) -> Result { + let input: ListFunctionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_function::builders::ListFunctionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListFunctionOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_function() -> ToolInfo { + ToolInfo { + name: "ListFunction".to_string(), + description: "Retrieves a paginated list of all functions in the workspace with their basic information and current status.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_type": { + "type": "array", + "items": { + "type": "string", + "enum": ["VALUE_VALIDATION", "VALUE_COMPUTE", "CONTEXT_VALIDATION", "CHANGE_REASON_VALIDATION"] + } + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_organisation(client: &Client, params: serde_json::Value) -> Result { + let input: ListOrganisationInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_organisation::builders::ListOrganisationInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListOrganisationOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_organisation() -> ToolInfo { + ToolInfo { + name: "ListOrganisation".to_string(), + description: "Retrieves a paginated list of all organisations with their basic information, creation details, and current status.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" } + }, + "required": [] +}), + } +} + +pub async fn handle_list_secrets(client: &Client, params: serde_json::Value) -> Result { + let input: ListSecretsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_secrets::builders::ListSecretsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListSecretsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_secrets() -> ToolInfo { + ToolInfo { + name: "ListSecrets".to_string(), + description: "Retrieves a paginated list of all secrets in the workspace with optional filtering and sorting. All secret values are masked.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { + "type": "array", + "items": { "type": "string" } + }, + "created_by": { + "type": "array", + "items": { "type": "string" } + }, + "last_modified_by": { + "type": "array", + "items": { "type": "string" } + }, + "sort_on": { + "type": "string", + "enum": ["name", "created_at", "last_modified_at"] + }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_variables(client: &Client, params: serde_json::Value) -> Result { + let input: ListVariablesInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_variables::builders::ListVariablesInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListVariablesOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_variables() -> ToolInfo { + ToolInfo { + name: "ListVariables".to_string(), + description: "Retrieves a paginated list of all variables in the workspace with optional filtering and sorting.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { + "type": "array", + "items": { "type": "string" } + }, + "created_by": { + "type": "array", + "items": { "type": "string" } + }, + "last_modified_by": { + "type": "array", + "items": { "type": "string" } + }, + "sort_on": { + "type": "string", + "enum": ["name", "created_at", "last_modified_at"] + }, + "sort_by": { + "type": "string", + "enum": ["desc", "asc"] + } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_versions(client: &Client, params: serde_json::Value) -> Result { + let input: ListVersionsInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_versions::builders::ListVersionsInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListVersionsOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_versions() -> ToolInfo { + ToolInfo { + name: "ListVersions".to_string(), + description: "Retrieves a paginated list of config versions with their metadata, hash values, and creation timestamps for audit and rollback purposes.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "count": { "type": "integer" }, + "page": { "type": "integer" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_webhook(client: &Client, params: serde_json::Value) -> Result { + let input: ListWebhookInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_webhook::builders::ListWebhookInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListWebhookOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_webhook() -> ToolInfo { + ToolInfo { + name: "ListWebhook".to_string(), + description: "Retrieves a paginated list of all webhook configs in the workspace, including their status and config details.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + +pub async fn handle_list_workspace(client: &Client, params: serde_json::Value) -> Result { + let input: ListWorkspaceInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::list_workspace::builders::ListWorkspaceInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ListWorkspaceOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_list_workspace() -> ToolInfo { + ToolInfo { + name: "ListWorkspace".to_string(), + description: "Retrieves a paginated list of all workspaces with optional filtering by workspace name, including their status, config details, and administrative information.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "count": { "type": "integer" }, + "page": { "type": "integer" }, + "all": { "type": "boolean" }, + "org_id": { "type": "string" } + }, + "required": ["org_id"] +}), + } +} + +pub async fn handle_migrate_workspace_schema(client: &Client, params: serde_json::Value) -> Result { + let input: WorkspaceSelectorRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::migrate_workspace_schema::builders::MigrateWorkspaceSchemaInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WorkspaceResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_migrate_workspace_schema() -> ToolInfo { + ToolInfo { + name: "MigrateWorkspaceSchema".to_string(), + description: "Migrates the workspace database schema to the new version of the template".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "org_id": { "type": "string" }, + "workspace_name": { "type": "string" } + }, + "required": ["org_id", "workspace_name"] +}), + } +} + +pub async fn handle_move_context(client: &Client, params: serde_json::Value) -> Result { + let input: MoveContextInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::move_context::builders::MoveContextInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ContextResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_move_context() -> ToolInfo { + ToolInfo { + name: "MoveContext".to_string(), + description: "Updates the condition of the mentioned context, if a context with the new condition already exists, it merges the override and effectively deleting the old context".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "request": { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "change_reason"] + } + }, + "required": ["workspace_id", "org_id", "id", "request"] +}), + } +} + +pub async fn handle_pause_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: PauseExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::pause_experiment::builders::PauseExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_pause_experiment() -> ToolInfo { + ToolInfo { + name: "PauseExperiment".to_string(), + description: "Temporarily pauses an inprogress experiment, suspending its effects while preserving the experiment config for later resumption.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id", "change_reason"] +}), + } +} + +pub async fn handle_publish(client: &Client, params: serde_json::Value) -> Result { + let input: PublishInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::publish::builders::PublishInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: FunctionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_publish() -> ToolInfo { + ToolInfo { + name: "Publish".to_string(), + description: "Publishes the draft version of a function, making it the active version used for value_validation, value_compute, context_validation or change_reason_validation in the system.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "function_name", "change_reason"] +}), + } +} + +pub async fn handle_ramp_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: RampExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::ramp_experiment::builders::RampExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_ramp_experiment() -> ToolInfo { + ToolInfo { + name: "RampExperiment".to_string(), + description: "Adjusts the traffic percentage allocation for an in-progress experiment, allowing gradual rollout or rollback of experimental features.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" }, + "traffic_percentage": { "type": "integer" } + }, + "required": ["workspace_id", "org_id", "id", "change_reason", "traffic_percentage"] +}), + } +} + +pub async fn handle_remove_members_from_group(client: &Client, params: serde_json::Value) -> Result { + let input: ModifyMembersToGroupRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::remove_members_from_group::builders::RemoveMembersFromGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_remove_members_from_group() -> ToolInfo { + ToolInfo { + name: "RemoveMembersFromGroup".to_string(), + description: "Removes members from an existing experiment group.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" }, + "member_experiment_ids": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["workspace_id", "org_id", "id", "change_reason", "member_experiment_ids"] +}), + } +} + +pub async fn handle_resume_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: ResumeExperimentInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::resume_experiment::builders::ResumeExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_resume_experiment() -> ToolInfo { + ToolInfo { + name: "ResumeExperiment".to_string(), + description: "Resumes a previously paused experiment, restoring its in-progress state and re-enabling variant evaluation.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id", "change_reason"] +}), + } +} + +pub async fn handle_rotate_master_encryption_key(client: &Client, params: serde_json::Value) -> Result { + let input: Unit = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::rotate_master_encryption_key::builders::RotateMasterEncryptionKeyInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: RotateMasterEncryptionKeyOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_rotate_master_encryption_key() -> ToolInfo { + ToolInfo { + name: "RotateMasterEncryptionKey".to_string(), + description: "Rotates the master encryption key across all workspaces".to_string(), + input_schema: serde_json::json!({ "type": "null" }), + } +} + +pub async fn handle_rotate_workspace_encryption_key(client: &Client, params: serde_json::Value) -> Result { + let input: WorkspaceSelectorRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::rotate_workspace_encryption_key::builders::RotateWorkspaceEncryptionKeyInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: RotateWorkspaceEncryptionKeyOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_rotate_workspace_encryption_key() -> ToolInfo { + ToolInfo { + name: "RotateWorkspaceEncryptionKey".to_string(), + description: "Rotates the workspace encryption key. Generates a new encryption key and re-encrypts all secrets with the new key. This is a critical operation that should be done during low-traffic periods.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "org_id": { "type": "string" }, + "workspace_name": { "type": "string" } + }, + "required": ["org_id", "workspace_name"] +}), + } +} + +pub async fn handle_test(client: &Client, params: serde_json::Value) -> Result { + let input: TestInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::test::builders::TestInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: FunctionExecutionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_test() -> ToolInfo { + ToolInfo { + name: "Test".to_string(), + description: "Executes a function in test mode with provided input parameters to validate its behavior before publishing or deployment.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" }, + "stage": { + "type": "string", + "enum": ["draft", "published"] + }, + "request": { + "oneOf": [ + { + "type": "object", + "properties": { + "value_validate": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "object" }, + "type": { "type": "string" }, + "environment": { "type": "object" } + }, + "required": ["key", "value", "type", "environment"] + } + }, + "required": ["value_validate"] + }, + { + "type": "object", + "properties": { + "value_compute": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "prefix": { "type": "string" }, + "type": { "type": "string" }, + "environment": { "type": "object" } + }, + "required": ["name", "prefix", "type", "environment"] + } + }, + "required": ["value_compute"] + }, + { + "type": "object", + "properties": { + "context_validate": { + "type": "object", + "properties": { + "environment": { "type": "object" } + }, + "required": ["environment"] + } + }, + "required": ["context_validate"] + }, + { + "type": "object", + "properties": { + "change_reason_validate": { + "type": "object", + "properties": { + "change_reason": { "type": "string" } + }, + "required": ["change_reason"] + } + }, + "required": ["change_reason_validate"] + } + ] + } + }, + "required": ["workspace_id", "org_id", "function_name", "stage", "request"] +}), + } +} + +pub async fn handle_update_default_config(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateDefaultConfigInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_default_config::builders::UpdateDefaultConfigInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DefaultConfigResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_default_config() -> ToolInfo { + ToolInfo { + name: "UpdateDefaultConfig".to_string(), + description: "Updates an existing default config entry. Allows modification of value, schema, function mappings, and description while preserving the key identifier.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "key": { "type": "string" }, + "change_reason": { "type": "string" }, + "value": { "type": "object" }, + "schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "value_validation_function_name": { "type": "string" }, + "description": { "type": "string" }, + "value_compute_function_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "key", "change_reason"] +}), + } +} + +pub async fn handle_update_dimension(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateDimensionInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_dimension::builders::UpdateDimensionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: DimensionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_dimension() -> ToolInfo { + ToolInfo { + name: "UpdateDimension".to_string(), + description: "Updates an existing dimension's configuration. Allows modification of schema, position, function mappings, and other properties while maintaining dependency relationships.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "dimension": { "type": "string" }, + "schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "position": { "type": "integer" }, + "value_validation_function_name": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "value_compute_function_name": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "dimension", "change_reason"] +}), + } +} + +pub async fn handle_update_experiment_group(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateExperimentGroupRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_experiment_group::builders::UpdateExperimentGroupInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentGroupResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_experiment_group() -> ToolInfo { + ToolInfo { + name: "UpdateExperimentGroup".to_string(), + description: "Updates an existing experiment group. Allows partial updates to specified fields.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "change_reason": { "type": "string" }, + "description": { "type": "string" }, + "traffic_percentage": { "type": "integer" } + }, + "required": ["workspace_id", "org_id", "id", "change_reason"] +}), + } +} + +pub async fn handle_update_function(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateFunctionRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_function::builders::UpdateFunctionInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: FunctionResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_function() -> ToolInfo { + ToolInfo { + name: "UpdateFunction".to_string(), + description: "Updates the draft version of an existing function with new code, runtime version, or description while preserving the published version.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "function_name": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "function": { "type": "string" }, + "runtime_version": { + "type": "string", + "enum": ["1.0"] + } + }, + "required": ["workspace_id", "org_id", "function_name", "change_reason"] +}), + } +} + +pub async fn handle_update_organisation(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateOrganisationRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_organisation::builders::UpdateOrganisationInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: OrganisationResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_organisation() -> ToolInfo { + ToolInfo { + name: "UpdateOrganisation".to_string(), + description: "Updates an existing organisation's information including contact details, status, and administrative properties.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "country_code": { "type": "string" }, + "contact_email": { "type": "string" }, + "contact_phone": { "type": "string" }, + "admin_email": { "type": "string" }, + "sector": { "type": "string" }, + "id": { "type": "string" }, + "status": { + "type": "string", + "enum": ["Active", "Inactive", "PendingKyb"] + } + }, + "required": ["id"] +}), + } +} + +pub async fn handle_update_override(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateOverrideInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_override::builders::UpdateOverrideInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ContextResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_override() -> ToolInfo { + ToolInfo { + name: "UpdateOverride".to_string(), + description: "Updates the overrides for an existing context. Allows modification of override values while maintaining the context's conditions.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "config_tags": { "type": "string" }, + "request": { + "type": "object", + "properties": { + "context": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" } + }, + "required": ["id"] + }, + { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["context"] + } + ] + }, + "override": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["context", "override", "change_reason"] + } + }, + "required": ["workspace_id", "org_id", "request"] +}), + } +} + +pub async fn handle_update_overrides_experiment(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateOverrideRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_overrides_experiment::builders::UpdateOverridesExperimentInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: ExperimentResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_overrides_experiment() -> ToolInfo { + ToolInfo { + name: "UpdateOverridesExperiment".to_string(), + description: "Updates the overrides for specific variants within an experiment, allowing modification of experiment behavior Updates the overrides for specific variants within an experiment, allowing modification of experiment behavior while it is in the created state.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "id": { "type": "string" }, + "variant_list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "overrides": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["id", "overrides"] + } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" }, + "metrics": { "type": "object" }, + "experiment_group_id": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "id", "variant_list", "change_reason"] +}), + } +} + +pub async fn handle_update_secret(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateSecretInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_secret::builders::UpdateSecretInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: SecretResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_secret() -> ToolInfo { + ToolInfo { + name: "UpdateSecret".to_string(), + description: "Updates an existing secret's value or description. The value is re-encrypted with the current workspace encryption key. Returns masked value.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "value": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "change_reason"] +}), + } +} + +pub async fn handle_update_type_templates(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateTypeTemplatesRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_type_templates::builders::UpdateTypeTemplatesInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: TypeTemplatesResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_type_templates() -> ToolInfo { + ToolInfo { + name: "UpdateTypeTemplates".to_string(), + description: "Updates an existing type template's schema definition and metadata while preserving its identifier and usage history.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "type_name": { "type": "string" }, + "type_schema": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "type_name", "type_schema", "change_reason"] +}), + } +} + +pub async fn handle_update_variable(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateVariableInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_variable::builders::UpdateVariableInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: VariableResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_variable() -> ToolInfo { + ToolInfo { + name: "UpdateVariable".to_string(), + description: "Updates an existing variable's value, description, or tags.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "value": { "type": "string" }, + "description": { "type": "string" }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "change_reason"] +}), + } +} + +pub async fn handle_update_webhook(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateWebhookInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_webhook::builders::UpdateWebhookInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WebhookResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_webhook() -> ToolInfo { + ToolInfo { + name: "UpdateWebhook".to_string(), + description: "Updates an existing webhook config, allowing modification of URL, events, headers, and other webhook properties.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "name": { "type": "string" }, + "description": { "type": "string" }, + "enabled": { "type": "boolean" }, + "url": { "type": "string" }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] + }, + "version": { + "type": "string", + "enum": ["V1"] + }, + "custom_headers": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "events": { + "type": "array", + "items": { "type": "string" } + }, + "change_reason": { "type": "string" } + }, + "required": ["workspace_id", "org_id", "name", "change_reason"] +}), + } +} + +pub async fn handle_update_workspace(client: &Client, params: serde_json::Value) -> Result { + let input: UpdateWorkspaceRequest = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::update_workspace::builders::UpdateWorkspaceInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WorkspaceResponse = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_update_workspace() -> ToolInfo { + ToolInfo { + name: "UpdateWorkspace".to_string(), + description: "Updates an existing workspace configuration, allowing modification of admin settings, mandatory dimensions, and workspace properties. Validates config version existence if provided.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "org_id": { "type": "string" }, + "workspace_name": { "type": "string" }, + "workspace_admin_email": { "type": "string" }, + "config_version": { "type": "string" }, + "mandatory_dimensions": { + "type": "array", + "items": { "type": "string" } + }, + "workspace_status": { + "type": "string", + "enum": ["ENABLED", "DISABLED"] + }, + "metrics": { "type": "object" }, + "allow_experiment_self_approval": { "type": "boolean" }, + "auto_populate_control": { "type": "boolean" }, + "enable_context_validation": { "type": "boolean" }, + "enable_change_reason_validation": { "type": "boolean" } + }, + "required": ["org_id", "workspace_name"] +}), + } +} + +pub async fn handle_validate_context(client: &Client, params: serde_json::Value) -> Result { + let input: ValidateContextInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::validate_context::builders::ValidateContextInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: Unit = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_validate_context() -> ToolInfo { + ToolInfo { + name: "ValidateContext".to_string(), + description: "Validates if a given context condition is well-formed".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "context": { + "type": "object", + "additionalProperties": { "type": "object" } + } + }, + "required": ["workspace_id", "org_id", "context"] +}), + } +} + +pub async fn handle_weight_recompute(client: &Client, params: serde_json::Value) -> Result { + let input: WeightRecomputeInput = serde_json::from_value(params)?; + let builder: superposition_sdk::operation::weight_recompute::builders::WeightRecomputeInputBuilder = input.into(); + let sdk_output = builder.send_with(client).await + .map_err(|e| McpError::internal(e.to_string()))?; + let mcp_output: WeightRecomputeOutput = sdk_output.into(); + Ok(serde_json::to_value(mcp_output)?) +} + +pub fn tool_info_weight_recompute() -> ToolInfo { + ToolInfo { + name: "WeightRecompute".to_string(), + description: "Recalculates and updates the priority weights for all contexts in the workspace based on their dimensions.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "workspace_id": { "type": "string" }, + "org_id": { "type": "string" }, + "config_tags": { "type": "string" } + }, + "required": ["workspace_id", "org_id"] +}), + } +} + diff --git a/crates/superposition_mcp/src/types.rs b/crates/superposition_mcp/src/types.rs new file mode 100644 index 000000000..07b83fd3a --- /dev/null +++ b/crates/superposition_mcp/src/types.rs @@ -0,0 +1,1560 @@ +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GroupType { + #[serde(rename = "USER_CREATED")] + UserCreated, + #[serde(rename = "SYSTEM_GENERATED")] + SystemGenerated, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum VariantType { + #[serde(rename = "CONTROL")] + Control, + #[serde(rename = "EXPERIMENTAL")] + Experimental, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExperimentType { + #[serde(rename = "DEFAULT")] + Default, + #[serde(rename = "DELETE_OVERRIDES")] + DeleteOverrides, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExperimentStatusType { + #[serde(rename = "CREATED")] + Created, + #[serde(rename = "CONCLUDED")] + Concluded, + #[serde(rename = "INPROGRESS")] + Inprogress, + #[serde(rename = "DISCARDED")] + Discarded, + #[serde(rename = "PAUSED")] + Paused, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FunctionRuntimeVersion { + #[serde(rename = "1.0")] + V1, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FunctionTypes { + #[serde(rename = "VALUE_VALIDATION")] + ValueValidation, + #[serde(rename = "VALUE_COMPUTE")] + ValueCompute, + #[serde(rename = "CONTEXT_VALIDATION")] + ContextValidation, + #[serde(rename = "CHANGE_REASON_VALIDATION")] + ChangeReasonValidation, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum OrgStatus { + #[serde(rename = "Active")] + Active, + #[serde(rename = "Inactive")] + Inactive, + #[serde(rename = "PendingKyb")] + PendingKyb, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HttpMethod { + #[serde(rename = "GET")] + Get, + #[serde(rename = "POST")] + Post, + #[serde(rename = "PUT")] + Put, + #[serde(rename = "PATCH")] + Patch, + #[serde(rename = "DELETE")] + Delete, + #[serde(rename = "HEAD")] + Head, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Version { + #[serde(rename = "V1")] + V1, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WorkspaceStatus { + #[serde(rename = "ENABLED")] + Enabled, + #[serde(rename = "DISABLED")] + Disabled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DimensionMatchStrategy { + #[serde(rename = "exact")] + Exact, + #[serde(rename = "subset")] + Subset, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MergeStrategy { + #[serde(rename = "MERGE")] + Merge, + #[serde(rename = "REPLACE")] + Replace, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuditAction { + #[serde(rename = "INSERT")] + Insert, + #[serde(rename = "UPDATE")] + Update, + #[serde(rename = "DELETE")] + Delete, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SortBy { + #[serde(rename = "desc")] + Desc, + #[serde(rename = "asc")] + Asc, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ContextFilterSortOn { + #[serde(rename = "last_modified_at")] + LastModifiedAt, + #[serde(rename = "created_at")] + CreatedAt, + #[serde(rename = "weight")] + Weight, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExperimentSortOn { + #[serde(rename = "last_modified_at")] + LastModifiedAt, + #[serde(rename = "created_at")] + CreatedAt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExperimentGroupSortOn { + #[serde(rename = "name")] + Name, + #[serde(rename = "created_at")] + CreatedAt, + #[serde(rename = "last_modified_at")] + LastModifiedAt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SecretSortOn { + #[serde(rename = "name")] + Name, + #[serde(rename = "created_at")] + CreatedAt, + #[serde(rename = "last_modified_at")] + LastModifiedAt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum VariableSortOn { + #[serde(rename = "name")] + Name, + #[serde(rename = "created_at")] + CreatedAt, + #[serde(rename = "last_modified_at")] + LastModifiedAt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Stage { + #[serde(rename = "draft")] + Draft, + #[serde(rename = "published")] + Published, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ContextAction { + #[serde(rename = "PUT")] + Put(ContextPut), + #[serde(rename = "REPLACE")] + Replace(UpdateContextOverrideRequest), + #[serde(rename = "DELETE")] + Delete(String), + #[serde(rename = "MOVE")] + Move(ContextMoveBulkRequest), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ContextIdentifier { + #[serde(rename = "id")] + Id(String), + #[serde(rename = "context")] + Context(HashMap), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ContextActionOut { + #[serde(rename = "PUT")] + Put(ContextResponse), + #[serde(rename = "REPLACE")] + Replace(ContextResponse), + #[serde(rename = "DELETE")] + Delete(String), + #[serde(rename = "MOVE")] + Move(ContextResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DimensionType { + #[serde(rename = "REGULAR")] + Regular, + #[serde(rename = "LOCAL_COHORT")] + LocalCohort(String), + #[serde(rename = "REMOTE_COHORT")] + RemoteCohort(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FunctionExecutionRequest { + #[serde(rename = "value_validate")] + ValueValidate(ValueValidationFunctionRequest), + #[serde(rename = "value_compute")] + ValueCompute(ValueComputeFunctionRequest), + #[serde(rename = "context_validate")] + ContextValidate(ContextValidationFunctionRequest), + #[serde(rename = "change_reason_validate")] + ChangeReasonValidate(ChangeReasonValidationFunctionRequest), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModifyMembersToGroupRequest { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, + pub member_experiment_ids: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExperimentGroupResponse { + pub id: String, + pub context_hash: String, + pub name: String, + pub description: String, + pub change_reason: String, + pub context: HashMap, + pub traffic_percentage: i32, + pub member_experiment_ids: Vec, + pub created_at: String, + pub created_by: String, + pub last_modified_at: String, + pub last_modified_by: String, + pub buckets: Vec, + pub group_type: GroupType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bucket { + pub experiment_id: String, + pub variant_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplicableVariantsInput { + pub workspace_id: String, + pub org_id: String, + pub context: HashMap, + pub identifier: String, + pub prefix: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplicableVariantsOutput { + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option, + pub override_id: Option, + pub overrides: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BulkOperationInput { + pub workspace_id: String, + pub org_id: String, + pub config_tags: Option, + pub operations: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextPut { + pub context: HashMap, + #[serde(rename = "override")] + pub r#override: HashMap, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateContextOverrideRequest { + pub context: ContextIdentifier, + #[serde(rename = "override")] + pub r#override: HashMap, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextMoveBulkRequest { + pub id: String, + pub request: ContextMove, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextMove { + pub context: HashMap, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BulkOperationOutput { + pub output: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextResponse { + pub id: String, + pub value: HashMap, + #[serde(rename = "override")] + pub r#override: HashMap, + pub override_id: String, + pub weight: String, + pub description: String, + pub change_reason: String, + pub created_at: String, + pub created_by: String, + pub last_modified_at: String, + pub last_modified_by: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConcludeExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub chosen_variant: String, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: String, + pub created_by: String, + pub last_modified: String, + pub name: String, + pub experiment_type: ExperimentType, + pub override_keys: Vec, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, + pub context: HashMap, + pub variants: Vec, + pub last_modified_by: String, + pub chosen_variant: Option, + pub description: String, + pub change_reason: String, + pub started_at: Option, + pub started_by: Option, + pub metrics_url: Option, + pub metrics: Option, + pub experiment_group_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateContextInput { + pub workspace_id: String, + pub org_id: String, + pub config_tags: Option, + pub request: ContextPut, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateDefaultConfigInput { + pub key: String, + pub value: serde_json::Value, + pub schema: HashMap, + pub description: String, + pub change_reason: String, + pub value_validation_function_name: Option, + pub value_compute_function_name: Option, + pub workspace_id: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DefaultConfigResponse { + pub key: String, + pub value: serde_json::Value, + pub schema: HashMap, + pub description: String, + pub change_reason: String, + pub value_validation_function_name: Option, + pub value_compute_function_name: Option, + pub created_at: String, + pub created_by: String, + pub last_modified_at: String, + pub last_modified_by: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateDimensionInput { + pub workspace_id: String, + pub org_id: String, + pub dimension: String, + pub position: i32, + pub schema: HashMap, + pub value_validation_function_name: Option, + pub description: String, + pub change_reason: String, + pub dimension_type: Option, + pub value_compute_function_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Unit { +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DimensionResponse { + pub dimension: String, + pub position: i32, + pub schema: HashMap, + pub value_validation_function_name: Option, + pub description: String, + pub change_reason: String, + pub last_modified_at: String, + pub last_modified_by: String, + pub created_at: String, + pub created_by: String, + pub dependency_graph: HashMap>, + pub dimension_type: DimensionType, + pub value_compute_function_name: Option, + pub mandatory: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateExperimentRequest { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub experiment_type: Option, + pub context: HashMap, + pub variants: Vec, + pub description: String, + pub change_reason: String, + pub metrics: Option, + pub experiment_group_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateExperimentGroupRequest { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub description: String, + pub change_reason: String, + pub context: HashMap, + pub traffic_percentage: i32, + pub member_experiment_ids: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateFunctionRequest { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, + pub description: String, + pub change_reason: String, + pub function: String, + pub runtime_version: FunctionRuntimeVersion, + pub function_type: FunctionTypes, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionResponse { + pub function_name: String, + pub published_code: Option, + pub draft_code: String, + pub published_runtime_version: Option, + pub draft_runtime_version: FunctionRuntimeVersion, + pub published_at: Option, + pub draft_edited_at: String, + pub published_by: Option, + pub draft_edited_by: String, + pub last_modified_at: String, + pub last_modified_by: String, + pub change_reason: String, + pub description: String, + pub function_type: FunctionTypes, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateOrganisationRequest { + pub country_code: Option, + pub contact_email: Option, + pub contact_phone: Option, + pub admin_email: String, + pub sector: Option, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrganisationResponse { + pub id: String, + pub name: String, + pub country_code: Option, + pub contact_email: Option, + pub contact_phone: Option, + pub created_by: String, + pub admin_email: String, + pub status: OrgStatus, + pub sector: Option, + pub created_at: String, + pub updated_at: String, + pub updated_by: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSecretInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub value: String, + pub description: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecretResponse { + pub name: String, + pub description: String, + pub change_reason: String, + pub created_by: String, + pub created_at: String, + pub last_modified_by: String, + pub last_modified_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateTypeTemplatesRequest { + pub workspace_id: String, + pub org_id: String, + pub type_name: String, + pub type_schema: HashMap, + pub description: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TypeTemplatesResponse { + pub type_name: String, + pub type_schema: HashMap, + pub description: String, + pub change_reason: String, + pub created_by: String, + pub created_at: String, + pub last_modified_at: String, + pub last_modified_by: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateVariableInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub value: String, + pub description: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VariableResponse { + pub name: String, + pub value: String, + pub description: String, + pub change_reason: String, + pub created_by: String, + pub created_at: String, + pub last_modified_by: String, + pub last_modified_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateWebhookInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub description: String, + pub enabled: bool, + pub url: String, + pub method: HttpMethod, + pub version: Option, + pub custom_headers: Option>, + pub events: Vec, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookResponse { + pub name: String, + pub description: String, + pub enabled: bool, + pub url: String, + pub method: HttpMethod, + pub version: Version, + pub custom_headers: Option>, + pub events: Vec, + pub max_retries: i32, + pub last_triggered_at: Option, + pub change_reason: String, + pub created_by: String, + pub created_at: String, + pub last_modified_by: String, + pub last_modified_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateWorkspaceRequest { + pub org_id: String, + pub workspace_admin_email: String, + pub workspace_name: String, + pub workspace_status: Option, + pub metrics: Option, + pub allow_experiment_self_approval: Option, + pub auto_populate_control: Option, + pub enable_context_validation: Option, + pub enable_change_reason_validation: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkspaceResponse { + pub workspace_name: String, + pub organisation_id: String, + pub organisation_name: String, + pub workspace_schema_name: String, + pub workspace_status: WorkspaceStatus, + pub workspace_admin_email: String, + pub config_version: Option, + pub created_by: String, + pub last_modified_by: String, + pub last_modified_at: String, + pub created_at: String, + pub mandatory_dimensions: Option>, + pub metrics: serde_json::Value, + pub allow_experiment_self_approval: bool, + pub auto_populate_control: bool, + pub enable_context_validation: bool, + pub enable_change_reason_validation: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteContextInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub config_tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteDefaultConfigInput { + pub workspace_id: String, + pub org_id: String, + pub key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteDimensionInput { + pub workspace_id: String, + pub org_id: String, + pub dimension: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteExperimentGroupInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteFunctionInput { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteSecretInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteTypeTemplatesInput { + pub workspace_id: String, + pub org_id: String, + pub type_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteVariableInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteWebhookInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscardExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigInput { + pub workspace_id: String, + pub org_id: String, + pub prefix: Option>, + pub version: Option, + pub if_modified_since: Option, + pub context: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigOutput { + pub contexts: Vec, + pub overrides: HashMap>, + pub default_configs: HashMap, + pub dimensions: HashMap, + pub version: String, + pub last_modified: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextPartial { + pub id: String, + pub condition: HashMap, + pub priority: i32, + pub weight: i32, + pub override_with_keys: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DimensionInfo { + pub schema: HashMap, + pub position: i32, + pub dimension_type: DimensionType, + pub dependency_graph: HashMap>, + pub value_compute_function_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigJsonInput { + pub workspace_id: String, + pub org_id: String, + pub if_modified_since: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigJsonOutput { + pub json_config: String, + pub last_modified: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigTomlInput { + pub workspace_id: String, + pub org_id: String, + pub if_modified_since: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetConfigTomlOutput { + pub toml_config: String, + pub last_modified: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetContextInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetContextFromConditionInput { + pub workspace_id: String, + pub org_id: String, + pub context: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetDefaultConfigInput { + pub workspace_id: String, + pub org_id: String, + pub key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetDimensionInput { + pub workspace_id: String, + pub org_id: String, + pub dimension: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetExperimentConfigInput { + pub workspace_id: String, + pub org_id: String, + pub if_modified_since: Option, + pub prefix: Option>, + pub context: Option>, + pub dimension_match_strategy: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetExperimentConfigOutput { + pub last_modified: String, + pub experiments: Vec, + pub experiment_groups: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetExperimentGroupInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetFunctionInput { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetOrganisationInput { + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetResolvedConfigInput { + pub workspace_id: String, + pub org_id: String, + pub prefix: Option>, + pub version: Option, + pub show_reasoning: Option, + pub merge_strategy: Option, + pub context_id: Option, + pub resolve_remote: Option, + pub context: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetResolvedConfigOutput { + pub config: serde_json::Value, + pub version: String, + pub last_modified: String, + pub audit_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetResolvedConfigWithIdentifierInput { + pub workspace_id: String, + pub org_id: String, + pub prefix: Option>, + pub version: Option, + pub show_reasoning: Option, + pub merge_strategy: Option, + pub context_id: Option, + pub resolve_remote: Option, + pub context: Option>, + pub identifier: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetResolvedConfigWithIdentifierOutput { + pub config: serde_json::Value, + pub version: String, + pub last_modified: String, + pub audit_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetSecretInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetTypeTemplateInput { + pub workspace_id: String, + pub org_id: String, + pub type_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetTypeTemplatesListInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetTypeTemplatesListOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetVariableInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetVersionInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetVersionResponse { + pub id: String, + pub config: ConfigData, + pub config_hash: String, + pub created_at: String, + pub description: String, + pub tags: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigData { + pub contexts: Vec, + pub overrides: HashMap>, + pub default_configs: HashMap, + pub dimensions: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetWebhookInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetWebhookByEventInput { + pub workspace_id: String, + pub org_id: String, + pub event: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetWorkspaceInput { + pub org_id: String, + pub workspace_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListAuditLogsInput { + pub workspace_id: String, + pub org_id: String, + pub count: Option, + pub page: Option, + pub all: Option, + pub from_date: Option, + pub to_date: Option, + pub tables: Option>, + pub action: Option>, + pub username: Option, + pub sort_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListAuditLogsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditLogFull { + pub id: String, + pub table_name: String, + pub user_name: String, + pub timestamp: String, + pub action: AuditAction, + pub original_data: Option, + pub new_data: Option, + pub query: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListContextsInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub prefix: Option>, + pub sort_on: Option, + pub sort_by: Option, + pub created_by: Option>, + pub last_modified_by: Option>, + pub plaintext: Option, + pub dimension_match_strategy: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListContextsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDefaultConfigsInput { + pub workspace_id: String, + pub org_id: String, + pub count: Option, + pub page: Option, + pub all: Option, + pub name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDefaultConfigsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDimensionsInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDimensionsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListExperimentInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub if_modified_since: Option, + pub status: Option>, + pub from_date: Option, + pub to_date: Option, + pub experiment_name: Option, + pub experiment_ids: Option>, + pub experiment_group_ids: Option>, + pub created_by: Option>, + pub sort_on: Option, + pub sort_by: Option, + pub global_experiments_only: Option, + pub dimension_match_strategy: Option, + pub prefix: Option>, + pub context: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListExperimentOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, + pub last_modified: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListExperimentGroupsInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub if_modified_since: Option, + pub name: Option, + pub created_by: Option, + pub last_modified_by: Option, + pub sort_on: Option, + pub sort_by: Option, + pub group_type: Option>, + pub dimension_match_strategy: Option, + pub context: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListExperimentGroupsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, + pub last_modified: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListFunctionInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub function_type: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListFunctionOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListOrganisationInput { + pub count: Option, + pub page: Option, + pub all: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListOrganisationOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListSecretsInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub name: Option>, + pub created_by: Option>, + pub last_modified_by: Option>, + pub sort_on: Option, + pub sort_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListSecretsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListVariablesInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, + pub name: Option>, + pub created_by: Option>, + pub last_modified_by: Option>, + pub sort_on: Option, + pub sort_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListVariablesOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListVersionsInput { + pub workspace_id: String, + pub org_id: String, + pub count: Option, + pub page: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListVersionsOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListVersionsMember { + pub id: String, + pub config: ConfigData, + pub created_at: String, + pub description: String, + pub tags: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListWebhookInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub workspace_id: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListWebhookOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListWorkspaceInput { + pub count: Option, + pub page: Option, + pub all: Option, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListWorkspaceOutput { + pub total_pages: i32, + pub total_items: i32, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkspaceSelectorRequest { + pub org_id: String, + pub workspace_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveContextInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub request: ContextMove, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PauseExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishInput { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RampExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, + pub traffic_percentage: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResumeExperimentInput { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RotateMasterEncryptionKeyOutput { + pub workspaces_rotated: i64, + pub total_secrets_re_encrypted: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RotateWorkspaceEncryptionKeyOutput { + pub total_secrets_re_encrypted: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestInput { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, + pub stage: Stage, + pub request: FunctionExecutionRequest, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValueValidationFunctionRequest { + pub key: String, + pub value: serde_json::Value, + #[serde(rename = "type")] + pub r#type: String, + pub environment: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValueComputeFunctionRequest { + pub name: String, + pub prefix: String, + #[serde(rename = "type")] + pub r#type: String, + pub environment: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextValidationFunctionRequest { + pub environment: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChangeReasonValidationFunctionRequest { + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionExecutionResponse { + pub fn_output: serde_json::Value, + pub stdout: String, + pub function_type: FunctionTypes, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDefaultConfigInput { + pub workspace_id: String, + pub org_id: String, + pub key: String, + pub change_reason: String, + pub value: Option, + pub schema: Option>, + pub value_validation_function_name: Option, + pub description: Option, + pub value_compute_function_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDimensionInput { + pub workspace_id: String, + pub org_id: String, + pub dimension: String, + pub schema: Option>, + pub position: Option, + pub value_validation_function_name: Option, + pub description: Option, + pub change_reason: String, + pub value_compute_function_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateExperimentGroupRequest { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub change_reason: String, + pub description: Option, + pub traffic_percentage: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateFunctionRequest { + pub workspace_id: String, + pub org_id: String, + pub function_name: String, + pub description: Option, + pub change_reason: String, + pub function: Option, + pub runtime_version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateOrganisationRequest { + pub country_code: Option, + pub contact_email: Option, + pub contact_phone: Option, + pub admin_email: Option, + pub sector: Option, + pub id: String, + pub status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateOverrideInput { + pub workspace_id: String, + pub org_id: String, + pub config_tags: Option, + pub request: UpdateContextOverrideRequest, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateOverrideRequest { + pub workspace_id: String, + pub org_id: String, + pub id: String, + pub variant_list: Vec, + pub description: Option, + pub change_reason: String, + pub metrics: Option, + pub experiment_group_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateSecretInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub value: Option, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateTypeTemplatesRequest { + pub workspace_id: String, + pub org_id: String, + pub type_name: String, + pub type_schema: HashMap, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateVariableInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub value: Option, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateWebhookInput { + pub workspace_id: String, + pub org_id: String, + pub name: String, + pub description: Option, + pub enabled: Option, + pub url: Option, + pub method: Option, + pub version: Option, + pub custom_headers: Option>, + pub events: Option>, + pub change_reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateWorkspaceRequest { + pub org_id: String, + pub workspace_name: String, + pub workspace_admin_email: Option, + pub config_version: Option, + pub mandatory_dimensions: Option>, + pub workspace_status: Option, + pub metrics: Option, + pub allow_experiment_self_approval: Option, + pub auto_populate_control: Option, + pub enable_context_validation: Option, + pub enable_change_reason_validation: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidateContextInput { + pub workspace_id: String, + pub org_id: String, + pub context: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WeightRecomputeInput { + pub workspace_id: String, + pub org_id: String, + pub config_tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WeightRecomputeOutput { + pub data: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WeightRecomputeResponse { + pub id: String, + pub condition: HashMap, + pub old_weight: String, + pub new_weight: String, +} + diff --git a/crates/superposition_mcp_server/Cargo.toml b/crates/superposition_mcp_server/Cargo.toml new file mode 100644 index 000000000..90fb5c992 --- /dev/null +++ b/crates/superposition_mcp_server/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "superposition_mcp_server" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository = "https://github.com/juspay/superposition" +description = "MCP server binary exposing the superposition API as tools" + +[lib] +name = "superposition_mcp_server" +path = "src/lib.rs" + +[[bin]] +name = "superposition-mcp" +path = "src/main.rs" + +[dependencies] +superposition_mcp = { path = "../superposition_mcp" } +superposition_sdk = { path = "../superposition_sdk" } +smithy-mcp-runtime = { git = "https://github.com/juspay/smithy-mcp-generator.git", rev = "6ec7445755c1410c052947f00eef1dd65011aadc", features = ["stdio", "http"] } + +aws-smithy-runtime-api = "1.7.4" +aws-smithy-types = "1.3.0" + +anyhow = { workspace = true } +async-trait = "0.1" +clap = { version = "4", features = ["derive", "env"] } +rmcp = { version = "1.2", features = ["server", "transport-streamable-http-server"] } +secrecy = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = "1.0.57" +tokio = { version = "1", features = ["full"] } +tower = "0.5" +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +axum = "0.7" +http = "1" +base64 = { workspace = true } + +[dev-dependencies] +wiremock = "0.6" +aws-smithy-http-client = { version = "1", features = ["default-client"] } diff --git a/crates/superposition_mcp_server/src/auth.rs b/crates/superposition_mcp_server/src/auth.rs new file mode 100644 index 000000000..6569e4a19 --- /dev/null +++ b/crates/superposition_mcp_server/src/auth.rs @@ -0,0 +1,318 @@ +use base64::Engine; +use secrecy::{ExposeSecret, SecretString}; + +#[derive(Debug, Clone)] +pub enum AuthValue { + Bearer(SecretString), + Basic { user: String, pass: SecretString }, +} + +tokio::task_local! { + pub static SUPERPOSITION_AUTH: AuthValue; +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum AuthParseError { + #[error("missing Authorization header")] + Missing, + #[error("malformed Authorization header")] + Malformed, + #[error("unsupported authentication scheme")] + UnsupportedScheme, +} + +impl AuthValue { + /// Parse an `Authorization` header value. + /// Supports `Bearer ` and `Basic `. + pub fn parse_header(value: Option<&str>) -> Result { + let raw = value.ok_or(AuthParseError::Missing)?.trim(); + let (scheme, rest) = raw.split_once(' ').ok_or(AuthParseError::Malformed)?; + let scheme = scheme.to_ascii_lowercase(); + let rest = rest.trim(); + match scheme.as_str() { + "bearer" => { + if rest.is_empty() { + return Err(AuthParseError::Malformed); + } + Ok(AuthValue::Bearer(SecretString::new(rest.to_string().into()))) + } + "basic" => { + let decoded = base64::engine::general_purpose::STANDARD + .decode(rest) + .map_err(|_| AuthParseError::Malformed)?; + let decoded = String::from_utf8(decoded).map_err(|_| AuthParseError::Malformed)?; + let (user, pass) = decoded.split_once(':').ok_or(AuthParseError::Malformed)?; + Ok(AuthValue::Basic { + user: user.to_string(), + pass: SecretString::new(pass.to_string().into()), + }) + } + _ => Err(AuthParseError::UnsupportedScheme), + } + } + + /// Bearer-token string (only meaningful for Bearer variant). + pub fn bearer(&self) -> Option<&str> { + match self { + AuthValue::Bearer(t) => Some(t.expose_secret()), + _ => None, + } + } + + /// (user, pass) for Basic variant. + pub fn basic(&self) -> Option<(&str, &str)> { + match self { + AuthValue::Basic { user, pass } => Some((user, pass.expose_secret())), + _ => None, + } + } +} + +impl From for AuthValue { + fn from(c: crate::config::StaticCreds) -> Self { + match c { + crate::config::StaticCreds::Bearer(t) => AuthValue::Bearer(t), + crate::config::StaticCreds::Basic { user, pass } => AuthValue::Basic { user, pass }, + } + } +} + +use aws_smithy_runtime_api::client::identity::{ + http::{Login, Token}, + Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver, +}; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_types::config_bag::ConfigBag; + +/// Resolves bearer-token identity from the task-local, falling back to a static value if provided. +#[derive(Debug)] +pub struct BearerResolver { + pub fallback: Option, +} + +impl ResolveIdentity for BearerResolver { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::ready({ + let token = SUPERPOSITION_AUTH + .try_with(|v| v.bearer().map(|s| s.to_string())) + .ok() + .flatten() + .or_else(|| self.fallback.as_ref().map(|s| s.expose_secret().to_string())); + + match token { + Some(t) => Ok(Identity::new(Token::new(t, None), None)), + None => Err("no bearer credential in task-local or fallback".into()), + } + }) + } +} + +/// Resolves basic-auth identity from the task-local, falling back to a static value if provided. +#[derive(Debug)] +pub struct BasicResolver { + pub fallback: Option<(String, SecretString)>, +} + +impl ResolveIdentity for BasicResolver { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::ready({ + let login = SUPERPOSITION_AUTH + .try_with(|v| v.basic().map(|(u, p)| (u.to_string(), p.to_string()))) + .ok() + .flatten() + .or_else(|| { + self.fallback + .as_ref() + .map(|(u, p)| (u.clone(), p.expose_secret().to_string())) + }); + + match login { + Some((u, p)) => Ok(Identity::new(Login::new(u, p, None), None)), + None => Err("no basic credential in task-local or fallback".into()), + } + }) + } +} + +pub fn shared_bearer(fallback: Option) -> SharedIdentityResolver { + SharedIdentityResolver::new(BearerResolver { fallback }) +} + +pub fn shared_basic(fallback: Option<(String, SecretString)>) -> SharedIdentityResolver { + SharedIdentityResolver::new(BasicResolver { fallback }) +} + +// ---------- Auth scheme option resolver ---------- +// +// The smithy-rs SDK lists `HTTP_BASIC_AUTH_SCHEME_ID` first in every operation's +// auth-options vector (see `superposition_sdk::auth_plugin::DefaultAuthOptionsPlugin`). +// The orchestrator picks the first scheme whose identity resolver succeeds and does +// not fall back to a later option if the first one fails. That means a client sending +// `Authorization: Bearer …` would hit `BasicResolver` first, get an error, and never +// reach the bearer path. +// +// Fix: install a runtime plugin that overrides the default static resolver with one +// that consults `SUPERPOSITION_AUTH` at request time and returns the single scheme +// that matches the credential variant. The orchestrator then drives only the matching +// identity resolver. + +use std::borrow::Cow; + +use aws_smithy_runtime_api::client::auth::{ + http::{HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID}, + AuthSchemeId, AuthSchemeOptionResolverParams, ResolveAuthSchemeOptions, +}; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; +use aws_smithy_runtime_api::client::runtime_plugin::{Order, RuntimePlugin}; + +/// Auth scheme picked when the per-request `SUPERPOSITION_AUTH` task-local is +/// unset. Drawn from the static-credential variant configured at startup +/// (stdio mode, or HTTP + `--allow-static-auth` with a fallback). When `None`, +/// the resolver fails the request — appropriate for HTTP passthrough mode +/// where the caller must supply credentials per request. +#[derive(Debug, Clone, Copy)] +pub enum FallbackScheme { + Bearer, + Basic, +} + +impl FallbackScheme { + fn id(self) -> AuthSchemeId { + match self { + FallbackScheme::Bearer => HTTP_BEARER_AUTH_SCHEME_ID, + FallbackScheme::Basic => HTTP_BASIC_AUTH_SCHEME_ID, + } + } +} + +#[derive(Debug)] +pub struct TaskLocalAuthSchemeResolver { + fallback: Option, + cached_bearer: Vec, + cached_basic: Vec, +} + +impl TaskLocalAuthSchemeResolver { + pub fn new(fallback: Option) -> Self { + Self { + fallback, + cached_bearer: vec![HTTP_BEARER_AUTH_SCHEME_ID], + cached_basic: vec![HTTP_BASIC_AUTH_SCHEME_ID], + } + } + + fn options_for(&self, scheme: AuthSchemeId) -> &[AuthSchemeId] { + if scheme == HTTP_BEARER_AUTH_SCHEME_ID { + &self.cached_bearer + } else { + &self.cached_basic + } + } +} + +impl ResolveAuthSchemeOptions for TaskLocalAuthSchemeResolver { + fn resolve_auth_scheme_options( + &self, + _params: &AuthSchemeOptionResolverParams, + ) -> Result, aws_smithy_runtime_api::box_error::BoxError> { + let scheme = SUPERPOSITION_AUTH + .try_with(|v| match v { + AuthValue::Bearer(_) => HTTP_BEARER_AUTH_SCHEME_ID, + AuthValue::Basic { .. } => HTTP_BASIC_AUTH_SCHEME_ID, + }) + .ok() + .or_else(|| self.fallback.map(FallbackScheme::id)) + .ok_or("no Authorization header and no static fallback configured")?; + Ok(Cow::Borrowed(self.options_for(scheme))) + } +} + +/// A runtime plugin that overrides the SDK's default static auth-scheme-option +/// resolver with one that picks the scheme per-request from `SUPERPOSITION_AUTH`. +#[derive(Debug)] +pub struct TaskLocalAuthSchemePlugin { + runtime_components: RuntimeComponentsBuilder, +} + +impl TaskLocalAuthSchemePlugin { + pub fn new(fallback: Option) -> Self { + Self { + runtime_components: RuntimeComponentsBuilder::new("task_local_auth_scheme") + .with_auth_scheme_option_resolver(Some(TaskLocalAuthSchemeResolver::new(fallback))), + } + } +} + +impl RuntimePlugin for TaskLocalAuthSchemePlugin { + fn order(&self) -> Order { + // Override the default scheme resolver installed by the smithy-rs codegen. + Order::Overrides + } + + fn runtime_components( + &self, + _current_components: &RuntimeComponentsBuilder, + ) -> Cow<'_, RuntimeComponentsBuilder> { + Cow::Borrowed(&self.runtime_components) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_bearer() { + let v = AuthValue::parse_header(Some("Bearer abc123")).unwrap(); + assert_eq!(v.bearer(), Some("abc123")); + } + + #[test] + fn parses_bearer_case_insensitive_scheme() { + let v = AuthValue::parse_header(Some("bearer abc")).unwrap(); + assert_eq!(v.bearer(), Some("abc")); + } + + #[test] + fn parses_basic() { + let creds = base64::engine::general_purpose::STANDARD.encode("alice:s3cret"); + let v = AuthValue::parse_header(Some(&format!("Basic {}", creds))).unwrap(); + assert_eq!(v.basic(), Some(("alice", "s3cret"))); + } + + #[test] + fn rejects_missing() { + assert_eq!(AuthValue::parse_header(None).unwrap_err(), AuthParseError::Missing); + } + + #[test] + fn rejects_empty_bearer() { + assert_eq!( + AuthValue::parse_header(Some("Bearer ")).unwrap_err(), + AuthParseError::Malformed + ); + } + + #[test] + fn rejects_unknown_scheme() { + assert_eq!( + AuthValue::parse_header(Some("Digest xyz")).unwrap_err(), + AuthParseError::UnsupportedScheme + ); + } + + #[test] + fn rejects_malformed_basic_no_colon() { + let creds = base64::engine::general_purpose::STANDARD.encode("no-colon-here"); + let v = AuthValue::parse_header(Some(&format!("Basic {}", creds))); + assert_eq!(v.unwrap_err(), AuthParseError::Malformed); + } +} diff --git a/crates/superposition_mcp_server/src/build.rs b/crates/superposition_mcp_server/src/build.rs new file mode 100644 index 000000000..210ebcc5f --- /dev/null +++ b/crates/superposition_mcp_server/src/build.rs @@ -0,0 +1,175 @@ +use std::sync::Arc; + +use smithy_mcp_runtime::Router; +use superposition_mcp::tools; +use superposition_sdk::{config::Builder, Client}; + +use crate::auth::{shared_basic, shared_bearer, FallbackScheme, TaskLocalAuthSchemePlugin}; +use crate::config::{Config, Defaults, Mode, StaticCreds}; +use crate::dispatch; + +pub fn build_client(cfg: &Config, mode: Mode) -> Client { + build_client_with(cfg, mode, |b| b) +} + +/// Construct the SDK client, allowing the caller to apply additional +/// modifications to the [`Builder`] before `.build()`. Used by integration +/// tests to swap in a non-TLS HTTP client that can talk to wiremock; the +/// production binary uses [`build_client`] which preserves the default +/// HTTPS connector. +pub fn build_client_with(cfg: &Config, mode: Mode, customise: F) -> Client +where + F: FnOnce(Builder) -> Builder, +{ + let bearer_fallback = match (&cfg.creds, mode) { + (Some(StaticCreds::Bearer(t)), Mode::Stdio) + | (Some(StaticCreds::Bearer(t)), Mode::HttpWithStaticFallback) => Some(t.clone()), + _ => None, + }; + let basic_fallback = match (&cfg.creds, mode) { + (Some(StaticCreds::Basic { user, pass }), Mode::Stdio) + | (Some(StaticCreds::Basic { user, pass }), Mode::HttpWithStaticFallback) => { + Some((user.clone(), pass.clone())) + } + _ => None, + }; + + // The fallback scheme is the auth scheme used when the per-request + // `SUPERPOSITION_AUTH` task-local is unset — i.e., stdio mode (where the + // task-local is set process-wide from env creds) and HTTP + `--allow-static-auth` + // when no `Authorization` header is supplied. The scheme is derived from + // which credential variant the operator configured. + let fallback_scheme = cfg.creds.as_ref().map(|c| match c { + StaticCreds::Bearer(_) => FallbackScheme::Bearer, + StaticCreds::Basic { .. } => FallbackScheme::Basic, + }); + + let bearer = shared_bearer(bearer_fallback); + let basic = shared_basic(basic_fallback); + + let builder = Builder::new() + .behavior_version_latest() + .endpoint_url(&cfg.endpoint) + .bearer_token_resolver(bearer) + .basic_auth_login_resolver(basic) + .runtime_plugin(TaskLocalAuthSchemePlugin::new(fallback_scheme)); + + Client::from_conf(customise(builder).build()) +} + +pub fn build_router(client: Client, defaults: Defaults) -> Router { + let client = Arc::new(client); + let defaults = Arc::new(defaults); + let mut router = Router::new(); + + macro_rules! register { + ($info_fn:ident, $handle_fn:ident) => {{ + let c = client.clone(); + let d = defaults.clone(); + router.register_tool(tools::$info_fn(), move |params| { + let c = c.clone(); + let d = d.clone(); + async move { + let params = dispatch::inject_defaults(params, &d); + tools::$handle_fn(&c, params).await + } + }); + }}; + } + + // BEGIN GENERATED TOOL REGISTRATIONS + // + // Regenerate with: + // grep -E "^pub async fn handle_" crates/superposition_mcp/src/tools.rs \ + // | awk "{print \$4}" | sed "s/(.*//" | sort \ + // | sed "s/^handle_(.*)$/ register!(tool_info_, handle_);/" + // (replace with the regex backreference for your shell; see plan doc). + register!(tool_info_add_members_to_group, handle_add_members_to_group); + register!(tool_info_applicable_variants, handle_applicable_variants); + register!(tool_info_bulk_operation, handle_bulk_operation); + register!(tool_info_conclude_experiment, handle_conclude_experiment); + register!(tool_info_create_context, handle_create_context); + register!(tool_info_create_default_config, handle_create_default_config); + register!(tool_info_create_dimension, handle_create_dimension); + register!(tool_info_create_experiment, handle_create_experiment); + register!(tool_info_create_experiment_group, handle_create_experiment_group); + register!(tool_info_create_function, handle_create_function); + register!(tool_info_create_organisation, handle_create_organisation); + register!(tool_info_create_secret, handle_create_secret); + register!(tool_info_create_type_templates, handle_create_type_templates); + register!(tool_info_create_variable, handle_create_variable); + register!(tool_info_create_webhook, handle_create_webhook); + register!(tool_info_create_workspace, handle_create_workspace); + register!(tool_info_delete_context, handle_delete_context); + register!(tool_info_delete_default_config, handle_delete_default_config); + register!(tool_info_delete_dimension, handle_delete_dimension); + register!(tool_info_delete_experiment_group, handle_delete_experiment_group); + register!(tool_info_delete_function, handle_delete_function); + register!(tool_info_delete_secret, handle_delete_secret); + register!(tool_info_delete_type_templates, handle_delete_type_templates); + register!(tool_info_delete_variable, handle_delete_variable); + register!(tool_info_delete_webhook, handle_delete_webhook); + register!(tool_info_discard_experiment, handle_discard_experiment); + register!(tool_info_get_config, handle_get_config); + register!(tool_info_get_config_json, handle_get_config_json); + register!(tool_info_get_config_toml, handle_get_config_toml); + register!(tool_info_get_context, handle_get_context); + register!(tool_info_get_context_from_condition, handle_get_context_from_condition); + register!(tool_info_get_default_config, handle_get_default_config); + register!(tool_info_get_dimension, handle_get_dimension); + register!(tool_info_get_experiment, handle_get_experiment); + register!(tool_info_get_experiment_config, handle_get_experiment_config); + register!(tool_info_get_experiment_group, handle_get_experiment_group); + register!(tool_info_get_function, handle_get_function); + register!(tool_info_get_organisation, handle_get_organisation); + register!(tool_info_get_resolved_config, handle_get_resolved_config); + register!(tool_info_get_resolved_config_with_identifier, handle_get_resolved_config_with_identifier); + register!(tool_info_get_secret, handle_get_secret); + register!(tool_info_get_type_template, handle_get_type_template); + register!(tool_info_get_type_templates_list, handle_get_type_templates_list); + register!(tool_info_get_variable, handle_get_variable); + register!(tool_info_get_version, handle_get_version); + register!(tool_info_get_webhook, handle_get_webhook); + register!(tool_info_get_webhook_by_event, handle_get_webhook_by_event); + register!(tool_info_get_workspace, handle_get_workspace); + register!(tool_info_list_audit_logs, handle_list_audit_logs); + register!(tool_info_list_contexts, handle_list_contexts); + register!(tool_info_list_default_configs, handle_list_default_configs); + register!(tool_info_list_dimensions, handle_list_dimensions); + register!(tool_info_list_experiment, handle_list_experiment); + register!(tool_info_list_experiment_groups, handle_list_experiment_groups); + register!(tool_info_list_function, handle_list_function); + register!(tool_info_list_organisation, handle_list_organisation); + register!(tool_info_list_secrets, handle_list_secrets); + register!(tool_info_list_variables, handle_list_variables); + register!(tool_info_list_versions, handle_list_versions); + register!(tool_info_list_webhook, handle_list_webhook); + register!(tool_info_list_workspace, handle_list_workspace); + register!(tool_info_migrate_workspace_schema, handle_migrate_workspace_schema); + register!(tool_info_move_context, handle_move_context); + register!(tool_info_pause_experiment, handle_pause_experiment); + register!(tool_info_publish, handle_publish); + register!(tool_info_ramp_experiment, handle_ramp_experiment); + register!(tool_info_remove_members_from_group, handle_remove_members_from_group); + register!(tool_info_resume_experiment, handle_resume_experiment); + register!(tool_info_rotate_master_encryption_key, handle_rotate_master_encryption_key); + register!(tool_info_rotate_workspace_encryption_key, handle_rotate_workspace_encryption_key); + register!(tool_info_test, handle_test); + register!(tool_info_update_default_config, handle_update_default_config); + register!(tool_info_update_dimension, handle_update_dimension); + register!(tool_info_update_experiment_group, handle_update_experiment_group); + register!(tool_info_update_function, handle_update_function); + register!(tool_info_update_organisation, handle_update_organisation); + register!(tool_info_update_override, handle_update_override); + register!(tool_info_update_overrides_experiment, handle_update_overrides_experiment); + register!(tool_info_update_secret, handle_update_secret); + register!(tool_info_update_type_templates, handle_update_type_templates); + register!(tool_info_update_variable, handle_update_variable); + register!(tool_info_update_webhook, handle_update_webhook); + register!(tool_info_update_workspace, handle_update_workspace); + register!(tool_info_validate_context, handle_validate_context); + register!(tool_info_weight_recompute, handle_weight_recompute); + // END GENERATED TOOL REGISTRATIONS + + router +} diff --git a/crates/superposition_mcp_server/src/config.rs b/crates/superposition_mcp_server/src/config.rs new file mode 100644 index 000000000..e0bee1b94 --- /dev/null +++ b/crates/superposition_mcp_server/src/config.rs @@ -0,0 +1,177 @@ +use secrecy::SecretString; + +#[derive(Debug, Clone)] +pub enum StaticCreds { + Bearer(SecretString), + Basic { user: String, pass: SecretString }, +} + +impl StaticCreds { + pub fn is_bearer(&self) -> bool { + matches!(self, StaticCreds::Bearer(_)) + } + pub fn is_basic(&self) -> bool { + matches!(self, StaticCreds::Basic { .. }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Defaults { + pub workspace_id: Option, + pub org_id: Option, +} + +#[derive(Debug, Clone)] +pub struct Config { + pub endpoint: String, + pub creds: Option, + pub defaults: Defaults, +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConfigError { + #[error("SUPERPOSITION_ENDPOINT is required")] + MissingEndpoint, + #[error("provide either SUPERPOSITION_BEARER_TOKEN or SUPERPOSITION_BASIC_USER+SUPERPOSITION_BASIC_PASS, not both")] + ConflictingCreds, + #[error("SUPERPOSITION_BASIC_USER and SUPERPOSITION_BASIC_PASS must be set together")] + IncompleteBasic, + #[error("stdio mode requires credentials (bearer or basic)")] + StdioRequiresCreds, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Mode { + Stdio, + HttpPassthrough, + HttpWithStaticFallback, +} + +pub fn load(mode: Mode, env: &dyn EnvLookup) -> Result { + let endpoint = env.get("SUPERPOSITION_ENDPOINT").ok_or(ConfigError::MissingEndpoint)?; + + let bearer = env.get("SUPERPOSITION_BEARER_TOKEN"); + let basic_user = env.get("SUPERPOSITION_BASIC_USER"); + let basic_pass = env.get("SUPERPOSITION_BASIC_PASS"); + + let creds = match (bearer, basic_user, basic_pass) { + (Some(_), Some(_), _) | (Some(_), _, Some(_)) => return Err(ConfigError::ConflictingCreds), + (Some(b), None, None) => Some(StaticCreds::Bearer(SecretString::new(b.into()))), + (None, Some(u), Some(p)) => Some(StaticCreds::Basic { user: u, pass: SecretString::new(p.into()) }), + (None, Some(_), None) | (None, None, Some(_)) => return Err(ConfigError::IncompleteBasic), + (None, None, None) => None, + }; + + if matches!(mode, Mode::Stdio) && creds.is_none() { + return Err(ConfigError::StdioRequiresCreds); + } + + Ok(Config { + endpoint, + creds, + defaults: Defaults { + workspace_id: env.get("SUPERPOSITION_WORKSPACE_ID"), + org_id: env.get("SUPERPOSITION_ORG_ID"), + }, + }) +} + +pub trait EnvLookup { + fn get(&self, key: &str) -> Option; +} + +pub struct ProcessEnv; +impl EnvLookup for ProcessEnv { + fn get(&self, key: &str) -> Option { + std::env::var(key).ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct MapEnv(HashMap); + impl EnvLookup for MapEnv { + fn get(&self, key: &str) -> Option { + self.0.get(key).cloned() + } + } + fn env(pairs: &[(&str, &str)]) -> MapEnv { + MapEnv(pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()) + } + + #[test] + fn stdio_requires_endpoint() { + let e = env(&[("SUPERPOSITION_BEARER_TOKEN", "t")]); + assert_eq!(load(Mode::Stdio, &e).unwrap_err(), ConfigError::MissingEndpoint); + } + + #[test] + fn stdio_requires_creds() { + let e = env(&[("SUPERPOSITION_ENDPOINT", "https://api.example.com")]); + assert_eq!(load(Mode::Stdio, &e).unwrap_err(), ConfigError::StdioRequiresCreds); + } + + #[test] + fn stdio_accepts_bearer() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert!(c.creds.unwrap().is_bearer()); + } + + #[test] + fn stdio_accepts_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BASIC_USER", "u"), + ("SUPERPOSITION_BASIC_PASS", "p"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert!(c.creds.unwrap().is_basic()); + } + + #[test] + fn rejects_both_bearer_and_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ("SUPERPOSITION_BASIC_USER", "u"), + ("SUPERPOSITION_BASIC_PASS", "p"), + ]); + assert_eq!(load(Mode::Stdio, &e).unwrap_err(), ConfigError::ConflictingCreds); + } + + #[test] + fn rejects_incomplete_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BASIC_USER", "u"), + ]); + assert_eq!(load(Mode::Stdio, &e).unwrap_err(), ConfigError::IncompleteBasic); + } + + #[test] + fn http_passthrough_accepts_no_creds() { + let e = env(&[("SUPERPOSITION_ENDPOINT", "https://api.example.com")]); + let c = load(Mode::HttpPassthrough, &e).unwrap(); + assert!(c.creds.is_none()); + } + + #[test] + fn defaults_populate_from_env() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ("SUPERPOSITION_WORKSPACE_ID", "w1"), + ("SUPERPOSITION_ORG_ID", "o1"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert_eq!(c.defaults.workspace_id.as_deref(), Some("w1")); + assert_eq!(c.defaults.org_id.as_deref(), Some("o1")); + } +} diff --git a/crates/superposition_mcp_server/src/dispatch.rs b/crates/superposition_mcp_server/src/dispatch.rs new file mode 100644 index 000000000..5f858c739 --- /dev/null +++ b/crates/superposition_mcp_server/src/dispatch.rs @@ -0,0 +1,70 @@ +use serde_json::Value; + +use crate::config::Defaults; + +/// If the params is a JSON object and is missing `workspace_id` / `org_id`, +/// inject defaults from the configuration. Other shapes (non-object) are +/// passed through unchanged — the SDK call will surface a typed deserialize +/// error verbatim. +pub fn inject_defaults(params: Value, defaults: &Defaults) -> Value { + let Value::Object(mut obj) = params else { + return params; + }; + if let Some(w) = defaults.workspace_id.as_ref() { + obj.entry("workspace_id".to_string()) + .or_insert_with(|| Value::String(w.clone())); + } + if let Some(o) = defaults.org_id.as_ref() { + obj.entry("org_id".to_string()) + .or_insert_with(|| Value::String(o.clone())); + } + Value::Object(obj) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn defaults(w: Option<&str>, o: Option<&str>) -> Defaults { + Defaults { + workspace_id: w.map(String::from), + org_id: o.map(String::from), + } + } + + #[test] + fn injects_both_when_absent() { + let params = json!({"key": "v"}); + let out = inject_defaults(params, &defaults(Some("w"), Some("o"))); + assert_eq!(out, json!({"key": "v", "workspace_id": "w", "org_id": "o"})); + } + + #[test] + fn param_value_wins_over_default() { + let params = json!({"workspace_id": "explicit"}); + let out = inject_defaults(params, &defaults(Some("default"), None)); + assert_eq!(out, json!({"workspace_id": "explicit"})); + } + + #[test] + fn injects_org_only_when_only_org_defaulted() { + let params = json!({}); + let out = inject_defaults(params, &defaults(None, Some("o"))); + assert_eq!(out, json!({"org_id": "o"})); + } + + #[test] + fn no_change_when_no_defaults() { + let params = json!({"x": 1}); + let out = inject_defaults(params.clone(), &defaults(None, None)); + assert_eq!(out, params); + } + + #[test] + fn passes_through_non_object() { + let params = json!(42); + let out = inject_defaults(params.clone(), &defaults(Some("w"), Some("o"))); + assert_eq!(out, params); + } +} diff --git a/crates/superposition_mcp_server/src/lib.rs b/crates/superposition_mcp_server/src/lib.rs new file mode 100644 index 000000000..af2ad7a56 --- /dev/null +++ b/crates/superposition_mcp_server/src/lib.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod build; +pub mod config; +pub mod dispatch; diff --git a/crates/superposition_mcp_server/src/main.rs b/crates/superposition_mcp_server/src/main.rs new file mode 100644 index 000000000..cc8368f1e --- /dev/null +++ b/crates/superposition_mcp_server/src/main.rs @@ -0,0 +1,75 @@ +use superposition_mcp_server::{auth, build, config}; + +mod transport_http; + +use clap::Parser; +use smithy_mcp_runtime::Router; +use tracing_subscriber::EnvFilter; + +use crate::auth::{AuthValue, SUPERPOSITION_AUTH}; +use crate::config::{Config, Mode}; + +#[derive(Parser, Debug)] +#[command(name = "superposition-mcp", about = "MCP server for the Superposition API")] +struct Cli { + /// Bind address for HTTP+SSE transport. If unset, stdio is used. + #[arg(long, value_name = "ADDR")] + http: Option, + + /// In HTTP mode, fall back to env-var credentials when no Authorization header is present. + #[arg(long, requires = "http")] + allow_static_auth: bool, +} + +fn init_logging() { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(std::io::stderr) // STDOUT is the JSON-RPC channel on stdio + .init(); +} + +async fn stdio_serve(cfg: Config, router: Router) -> anyhow::Result<()> { + let auth_value: AuthValue = cfg + .creds + .clone() + .expect("config::load enforces creds presence for stdio mode") + .into(); + + SUPERPOSITION_AUTH + .scope(auth_value, async { + smithy_mcp_runtime::serve_stdio(router) + .await + .map_err(|e| anyhow::anyhow!("stdio transport error: {e}")) + }) + .await +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + init_logging(); + let cli = Cli::parse(); + + let mode = match (&cli.http, cli.allow_static_auth) { + (None, _) => Mode::Stdio, + (Some(_), true) => Mode::HttpWithStaticFallback, + (Some(_), false) => Mode::HttpPassthrough, + }; + + let cfg = config::load(mode, &config::ProcessEnv)?; + let client = build::build_client(&cfg, mode); + let router = build::build_router(client, cfg.defaults.clone()); + + match (mode, cli.http.as_deref()) { + (Mode::Stdio, _) => stdio_serve(cfg, router).await, + (Mode::HttpPassthrough, Some(addr)) | (Mode::HttpWithStaticFallback, Some(addr)) => { + let socket_addr: std::net::SocketAddr = addr + .parse() + .map_err(|e| anyhow::anyhow!("invalid --http address {}: {}", addr, e))?; + let static_fallback: Option = + cfg.creds.clone().map(Into::into); + transport_http::serve(socket_addr, mode, static_fallback, router).await + } + _ => unreachable!(), + } +} diff --git a/crates/superposition_mcp_server/src/transport_http.rs b/crates/superposition_mcp_server/src/transport_http.rs new file mode 100644 index 000000000..108e84205 --- /dev/null +++ b/crates/superposition_mcp_server/src/transport_http.rs @@ -0,0 +1,182 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use axum::body::Body; +use axum::extract::Request; +use axum::http::{header, StatusCode}; +use axum::middleware::{self, Next}; +use axum::response::Response; +use axum::Router as AxumRouter; +use rmcp::handler::server::ServerHandler; +use rmcp::model::{ + CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, ServerInfo, +}; +use rmcp::service::RequestContext; +use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; +use rmcp::transport::streamable_http_server::{ + StreamableHttpServerConfig, StreamableHttpService, +}; +use rmcp::{ErrorData, RoleServer}; +use smithy_mcp_runtime::Router; +use tracing::{error, info, warn}; + +use crate::auth::{AuthParseError, AuthValue, SUPERPOSITION_AUTH}; +use crate::config::Mode; + +/// `AuthRouter` wraps `smithy_mcp_runtime::Router` and re-enters the +/// `SUPERPOSITION_AUTH` task-local on every `call_tool` based on the inbound +/// HTTP `Authorization` header, which rmcp stores in `RequestContext::extensions` +/// as `http::request::Parts`. +/// +/// **Why a wrapper and not the Axum middleware's `scope`?** +/// rmcp's stateful HTTP transport spawns a long-running session task during the +/// `initialize` request. Subsequent requests (`tools/call`, `tools/list`, …) +/// are routed into that session task's queue — *not* dispatched in the Axum +/// middleware's task. So a `SUPERPOSITION_AUTH.scope(...)` wrapped around +/// `next.run(req)` in the middleware applies only to the initialize, and the +/// session task carries the initialize-time auth for the rest of its life. +/// Multi-tenant HTTP usage with per-request credentials needs the scope to +/// happen *inside* the handler that runs in whatever task rmcp dispatches us +/// in — that's this wrapper. +struct AuthRouter { + inner: Arc, + /// Used when the inbound request has no Authorization header but the + /// operator started the binary with `--allow-static-auth` and provided env + /// credentials. None in pure passthrough mode. + static_fallback: Option, +} + +impl ServerHandler for AuthRouter { + fn get_info(&self) -> ServerInfo { + self.inner.get_info() + } + + fn list_tools( + &self, + request: Option, + context: RequestContext, + ) -> impl std::future::Future> + Send + '_ { + self.inner.list_tools(request, context) + } + + fn call_tool( + &self, + request: CallToolRequestParams, + context: RequestContext, + ) -> impl std::future::Future> + Send + '_ { + let inner = self.inner.clone(); + let fallback = self.static_fallback.clone(); + async move { + let auth = extract_auth_from_context(&context).or(fallback); + match auth { + Some(a) => { + SUPERPOSITION_AUTH + .scope(a, async move { inner.call_tool(request, context).await }) + .await + } + None => { + // Resolver will surface a clearer error; this branch should be + // unreachable for HTTP (the Axum middleware rejects unauthenticated + // requests at the transport layer). + inner.call_tool(request, context).await + } + } + } + } +} + +/// Pull the parsed `AuthValue` out of `RequestContext`. rmcp stores +/// `http::request::Parts` in the context's extensions; we re-parse the +/// `Authorization` header from it. Cheap, and avoids coupling between the +/// Axum middleware and the rmcp handler. +fn extract_auth_from_context(context: &RequestContext) -> Option { + let parts = context.extensions.get::()?; + let raw = parts + .headers + .get(http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok())?; + AuthValue::parse_header(Some(raw)).ok() +} + +pub async fn serve( + addr: SocketAddr, + mode: Mode, + static_fallback: Option, + router: Router, +) -> anyhow::Result<()> { + let router = Arc::new(router); + let static_fallback_for_handler = if matches!(mode, Mode::HttpWithStaticFallback) { + static_fallback.clone() + } else { + None + }; + + let svc = StreamableHttpService::new( + { + let router = router.clone(); + let fallback = static_fallback_for_handler.clone(); + move || { + Ok::<_, std::io::Error>(AuthRouter { + inner: router.clone(), + static_fallback: fallback.clone(), + }) + } + }, + Arc::new(LocalSessionManager::default()), + StreamableHttpServerConfig::default(), + ); + + let allow_static = matches!(mode, Mode::HttpWithStaticFallback); + let fallback = Arc::new(static_fallback); + + let app = AxumRouter::new() + .nest_service("/mcp", svc) + .layer(middleware::from_fn(move |req: Request, next: Next| { + let fallback = fallback.clone(); + async move { auth_layer(req, next, allow_static, fallback).await } + })); + + info!(%addr, "MCP server listening (HTTP)"); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} + +/// Axum middleware that rejects requests without a usable Authorization +/// header before they reach rmcp. Cheap pre-filter at the HTTP layer. +/// +/// The actual `SUPERPOSITION_AUTH.scope` happens inside [`AuthRouter::call_tool`], +/// not here — see the doc on [`AuthRouter`] for why. +async fn auth_layer( + req: Request, + next: Next, + allow_static: bool, + fallback: Arc>, +) -> Response { + let header_value = req + .headers() + .get(header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(str::to_owned); + + match AuthValue::parse_header(header_value.as_deref()) { + Ok(_) => next.run(req).await, + Err(AuthParseError::Missing) if allow_static && fallback.is_some() => { + warn!("falling back to static credentials (--allow-static-auth)"); + next.run(req).await + } + Err(AuthParseError::Missing) => { + unauthorized("no Authorization header and no static fallback") + } + Err(e) => unauthorized(&format!("{}", e)), + } +} + +fn unauthorized(detail: &str) -> Response { + error!("auth rejected: {detail}"); + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(header::WWW_AUTHENTICATE, "Bearer realm=\"superposition-mcp\"") + .body(Body::from(detail.to_string())) + .unwrap() +} diff --git a/crates/superposition_mcp_server/tests/integration.rs b/crates/superposition_mcp_server/tests/integration.rs new file mode 100644 index 000000000..57bd2ea38 --- /dev/null +++ b/crates/superposition_mcp_server/tests/integration.rs @@ -0,0 +1,150 @@ +use secrecy::SecretString; +use serde_json::json; +use wiremock::matchers::{header_exists, method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use superposition_mcp_server::auth::{AuthValue, SUPERPOSITION_AUTH}; +use superposition_mcp_server::build::{build_client_with, build_router}; +use superposition_mcp_server::config::{Config, Defaults, Mode}; + +/// Builds a router pointed at the given endpoint. The default SDK HTTP client +/// is HTTPS-only (via rustls/aws-lc) which can't talk to wiremock's plain-HTTP +/// listener, so we swap in `build_http()` for tests. The auth resolver wiring +/// and tool registration is otherwise identical to the production code path. +async fn router_against(endpoint: &str) -> smithy_mcp_runtime::Router { + let cfg = Config { + endpoint: endpoint.to_string(), + creds: None, + defaults: Defaults { + workspace_id: None, + org_id: None, + }, + }; + let http_client = aws_smithy_http_client::Builder::new().build_http(); + let client = build_client_with(&cfg, Mode::HttpPassthrough, |b| b.http_client(http_client)); + build_router(client, cfg.defaults) +} + +fn count_operations_in_smithy() -> usize { + let workspace_root = { + let mut p = std::env::current_dir().unwrap(); + loop { + if p.join("smithy/models").exists() && p.join("Cargo.toml").exists() { + break p; + } + p = p.parent().expect("cargo workspace root").to_path_buf(); + } + }; + let mut count = 0usize; + for entry in std::fs::read_dir(workspace_root.join("smithy/models")).unwrap() { + let path = entry.unwrap().path(); + if path.extension().and_then(|s| s.to_str()) == Some("smithy") { + let body = std::fs::read_to_string(&path).unwrap(); + count += body.matches("@mcpTool").count(); + } + } + count +} + +#[tokio::test] +async fn tools_list_matches_smithy_annotation_count() { + let server = MockServer::start().await; + let router = router_against(&server.uri()).await; + assert_eq!( + router.tool_names().len(), + count_operations_in_smithy(), + "router tool count must equal @mcpTool annotation count in smithy/models" + ); +} + +/// Stand up wiremock with a `GetOrganisation` stub that asserts an authorization +/// header is present and returns a minimal valid `OrganisationResponse`. +async fn stub_get_organisation(server: &MockServer) { + Mock::given(method("GET")) + .and(path("/superposition/organisations/test-id")) + .and(header_exists("authorization")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id": "test-id", + "name": "Test Org", + "country_code": null, + "contact_email": null, + "contact_phone": null, + "created_by": "tester", + "admin_email": "admin@example.com", + "status": "Active", + "sector": null, + "created_at": "2026-05-11T12:00:00Z", + "updated_at": "2026-05-11T12:00:00Z", + "updated_by": "tester" + }))) + .mount(server) + .await; +} + +async fn call_get_organisation( + server_uri: &str, + auth: AuthValue, +) -> serde_json::Value { + let server_uri = server_uri.to_string(); + let router = router_against(&server_uri).await; + let result = SUPERPOSITION_AUTH + .scope(auth, async { + router + .test_call_tool("GetOrganisation", json!({"id": "test-id"})) + .await + }) + .await; + result.expect("tool call succeeded") +} + +/// Verifies Basic-auth credentials in the task-local flow through to the wire. +#[tokio::test] +async fn passthrough_basic_auth_reaches_the_wire() { + let server = MockServer::start().await; + stub_get_organisation(&server).await; + + let auth = AuthValue::Basic { + user: "alice".to_string(), + pass: SecretString::new("s3cret".to_string().into()), + }; + let value = call_get_organisation(&server.uri(), auth).await; + assert_eq!(value["id"], json!("test-id")); + + let received = server.received_requests().await.expect("received_requests"); + let auth_header = received[0] + .headers + .get("authorization") + .expect("authorization header present") + .to_str() + .unwrap(); + // base64("alice:s3cret") == "YWxpY2U6czNjcmV0" + assert_eq!(auth_header, "Basic YWxpY2U6czNjcmV0"); +} + +/// Verifies Bearer-token credentials in the task-local flow through to the wire. +/// +/// Regression test: previously, the smithy-generated SDK listed +/// `HTTP_BASIC_AUTH_SCHEME_ID` first in every operation's auth-options vector, +/// and the orchestrator does not fall back to subsequent schemes when the first +/// resolver returns `Err`. So a `Bearer` value in the task-local hit +/// `BasicResolver` first, errored, and never reached the wire. Fixed by +/// installing a `TaskLocalAuthSchemeResolver` that picks the scheme per request +/// based on the credential variant in `SUPERPOSITION_AUTH`. +#[tokio::test] +async fn passthrough_bearer_auth_reaches_the_wire() { + let server = MockServer::start().await; + stub_get_organisation(&server).await; + + let auth = AuthValue::Bearer(SecretString::new("tok-abc".to_string().into())); + let value = call_get_organisation(&server.uri(), auth).await; + assert_eq!(value["id"], json!("test-id")); + + let received = server.received_requests().await.expect("received_requests"); + let auth_header = received[0] + .headers + .get("authorization") + .expect("authorization header present") + .to_str() + .unwrap(); + assert_eq!(auth_header, "Bearer tok-abc"); +} diff --git a/docs/superpowers/plans/2026-05-11-superposition-mcp-server.md b/docs/superpowers/plans/2026-05-11-superposition-mcp-server.md new file mode 100644 index 000000000..12bf886ba --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-superposition-mcp-server.md @@ -0,0 +1,1875 @@ +# Superposition MCP Server Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a Smithy-generated MCP (Model Context Protocol) server to the superposition repo so MCP-capable clients (Claude Desktop, Claude Code, mcp-cli) can call superposition operations as tools. The server ships as a hand-written binary `superposition-mcp` that wraps a generated library crate, with stdio and HTTP+SSE transports. + +**Architecture:** Two new workspace crates — `crates/superposition_mcp` (pure smithy-mcp-generator output, regenerable, treated like `crates/superposition_sdk`) and `crates/superposition_mcp_server` (hand-written binary). The binary uses smithy-rs `Client` plus a custom `ResolveIdentity` implementation that reads credentials from a `tokio::task_local!` populated either at startup (stdio, static auth) or per-HTTP-request (passthrough). HTTP transport is implemented directly in the binary against rmcp's `StreamableHttpService` using the runtime's `into_router()` escape hatch, since `smithy-mcp-runtime`'s HTTP support is currently a placeholder. + +**Tech Stack:** Smithy IDL + smithy-mcp-generator (Java) for codegen; smithy-rs–generated `superposition_sdk` for HTTP; `smithy-mcp-runtime` (Rust) for the MCP `Router` and stdio transport; `rmcp` 1.2 directly for HTTP+SSE transport; Axum + Tower for HTTP serving; `secrecy` for credential storage; `tokio::task_local!` for per-request auth; `wiremock` for integration tests. + +**Reference spec:** `docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md`. Read it before starting Task 1; it explains the *why* behind each decision below. + +**Prerequisite:** [juspay/smithy-mcp-generator#5](https://github.com/juspay/smithy-mcp-generator/pull/5) (`feat: make @mcpTool description optional, fall back to @documentation`) must be merged into `main` of `juspay/smithy-mcp-generator` before Task 1. The PR itself was created during the brainstorming session that produced this plan. + +--- + +## File Structure + +| Path | Status | Responsibility | +|---|---|---| +| `smithy/models/*.smithy` (~16 files) | modify | Add `use software.amazon.smithy.mcp#mcpTool` + `@mcpTool` per operation. | +| `smithy/smithy-build.json` | modify | Add `mcp-rust` plugin block + Maven deps. | +| `smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/{jar,pom}` | create | Bundled codegen artifact (transitional). | +| `smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/{jar,pom}` | create | Bundled trait artifact (transitional). | +| `smithy/maven-local/README.md` | create | Build-from-source recipe and migration note. | +| `smithy/patches/mcp-rust.patch` | create | Rewrites generated `Cargo.toml` to inherit from workspace. | +| `makefile` | modify | Extend `smithy-clients`; add to `EXCLUDE_PACKAGES`; extend `SMITHY_MAVEN_REPOS`. | +| `Cargo.toml` | modify | Add two new workspace members. | +| `crates/superposition_mcp/` | create (regenerated) | Generator output. README + CHANGELOG are hand-preserved. | +| `crates/superposition_mcp_server/Cargo.toml` | create | Binary crate manifest. | +| `crates/superposition_mcp_server/src/main.rs` | create | CLI entry point; transport selection. | +| `crates/superposition_mcp_server/src/config.rs` | create | Env-var loading + validation. | +| `crates/superposition_mcp_server/src/auth.rs` | create | `AuthValue` enum, `parse_header`, task-local, `ResolveIdentity` impls. | +| `crates/superposition_mcp_server/src/dispatch.rs` | create | Default `workspace_id`/`org_id` injection. | +| `crates/superposition_mcp_server/src/transport_http.rs` | create | Axum mount + Tower middleware for HTTP+SSE transport. | +| `crates/superposition_mcp_server/tests/integration.rs` | create | wiremock-backed end-to-end test. | + +--- + +## Task 1: Build & bundle smithy-mcp-codegen JARs into smithy/maven-local + +**Files:** +- Create: `smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.jar` +- Create: `smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.pom` +- Create: `smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.jar` +- Create: `smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.pom` +- Create: `smithy/maven-local/README.md` + +This task assumes PR #5 has been merged into `juspay/smithy-mcp-generator` `main` and that `juspay/smithy-mcp-generator` is checked out at the merge commit at `../smithy-mcp-generator` relative to the superposition repo. + +- [ ] **Step 1: Verify the generator repo's gradle publishing config produces the right Maven coordinates.** + +Check the generator's `build.gradle` files for a `maven-publish` plugin block and the `publishing.publications` Maven coordinates. If absent in `smithy-mcp-codegen/build.gradle` or `smithy-mcp-traits/build.gradle`, add it before continuing. + +Expected `groupId` / `artifactId` / `version`: + +| Artifact | groupId | artifactId | version | +|---|---|---|---| +| codegen | `in.juspay.smithy` | `smithy-mcp-codegen` | `0.1.0` | +| traits | `in.juspay.smithy` | `smithy-mcp-traits` | `0.1.0` | + +If the publishing config needs to be added, edit `smithy-mcp-codegen/build.gradle` and `smithy-mcp-traits/build.gradle` to add: + +```groovy +plugins { + id 'java-library' + id 'maven-publish' +} + +group = 'in.juspay.smithy' +version = '0.1.0' + +publishing { + publications { + maven(MavenPublication) { + from components.java + } + } +} +``` + +Adjust group/version if the existing config differs. + +- [ ] **Step 2: Build and publish to local Maven from the generator repo.** + +Run: + +```bash +cd ../smithy-mcp-generator +./gradlew :smithy-mcp-traits:publishToMavenLocal :smithy-mcp-codegen:publishToMavenLocal +``` + +Expected: `BUILD SUCCESSFUL`. Artifacts appear under `~/.m2/repository/in/juspay/smithy/{smithy-mcp-codegen,smithy-mcp-traits}/0.1.0/`. + +- [ ] **Step 3: Copy artifacts into the superposition repo.** + +```bash +cd /Users/natarajankannan/src/superposition.other +mkdir -p smithy/maven-local/in/juspay/smithy +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-codegen smithy/maven-local/in/juspay/smithy/ +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-traits smithy/maven-local/in/juspay/smithy/ +ls smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/ +ls smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/ +``` + +Expected: each directory contains at least `*.jar` and `*.pom` files (additional `.module` and checksum files are fine to copy too — leave them). + +- [ ] **Step 4: Write `smithy/maven-local/README.md` documenting the recipe and the migration note.** + +```markdown +# smithy/maven-local + +Bundled local Maven repository for `smithy-mcp-codegen` and `smithy-mcp-traits`. + +## Why this is here + +The smithy-mcp-generator does not yet publish its artifacts to a public Maven repository (and GitHub Packages requires authentication for Maven, even for public repos). To unblock superposition's MCP server work without setting up new publishing infrastructure, the generator JARs are bundled here in standard Maven layout. `SMITHY_MAVEN_REPOS` in the makefile points at this directory via a `file://` URL. + +## How to refresh + +When the generator is updated and a new version needs to be vendored: + +\`\`\`bash +cd ../smithy-mcp-generator +git checkout +./gradlew :smithy-mcp-traits:publishToMavenLocal :smithy-mcp-codegen:publishToMavenLocal + +cd ../superposition +rm -rf smithy/maven-local/in/juspay/smithy/{smithy-mcp-codegen,smithy-mcp-traits} +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-codegen smithy/maven-local/in/juspay/smithy/ +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-traits smithy/maven-local/in/juspay/smithy/ +\`\`\` + +Bump `runtimeVersion` in `smithy/smithy-build.json` to match. + +## Migration plan + +This is transitional. See spec §11.4 — eventual home is either the juspay sandbox Maven repo (already in `SMITHY_MAVEN_REPOS`) or JitPack. When that lands, delete this directory and remove the `file://` entry from `SMITHY_MAVEN_REPOS`. +``` + +- [ ] **Step 5: Commit.** + +```bash +git add smithy/maven-local +git commit -m "build: vendor smithy-mcp-codegen and smithy-mcp-traits JARs + +Transitional local Maven repo for the MCP-server smithy build plugin +and trait. See smithy/maven-local/README.md for the build recipe and +migration note tracked in +docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md +§11.4." +``` + +--- + +## Task 2: Wire smithy-build.json + makefile for the mcp-rust plugin + +**Files:** +- Modify: `smithy/smithy-build.json` +- Modify: `makefile` + +- [ ] **Step 1: Add the mcp-rust plugin block to `smithy/smithy-build.json`.** + +Open `smithy/smithy-build.json`. In `maven.dependencies`, append two new entries: + +```json +"in.juspay.smithy:smithy-mcp-codegen:0.1.0", +"in.juspay.smithy:smithy-mcp-traits:0.1.0" +``` + +In the `plugins` object, add a new `"mcp-rust"` entry alongside the existing `"rust-client-codegen"`: + +```json +"mcp-rust": { + "service": "io.superposition#Superposition", + "package": "superposition_mcp", + "clientSdk": "smithy-rs", + "clientCrate": "superposition_sdk", + "runtimeVersion": "0.1.0" +} +``` + +- [ ] **Step 2: Extend `SMITHY_MAVEN_REPOS` in the makefile.** + +Open `makefile`. Find this line (currently line 52): + +```makefile +export SMITHY_MAVEN_REPOS = https://repo1.maven.org/maven2|https://sandbox.assets.juspay.in/smithy/m2 +``` + +Replace it with: + +```makefile +export SMITHY_MAVEN_REPOS = https://repo1.maven.org/maven2|https://sandbox.assets.juspay.in/smithy/m2|file://$(CURDIR)/smithy/maven-local +``` + +- [ ] **Step 3: Run `make smithy-build` to verify the plugin loads and runs without producing the MCP crate yet.** + +The smithy models don't have `@mcpTool` annotations yet, so the plugin should run successfully but emit nothing (or produce an empty/skeleton output) — `McpGeneratePlugin.java` returns early when no operations have the trait. + +```bash +make smithy-clean-build +ls -la smithy/output/source/ +``` + +Expected: `smithy/output/source/` contains the existing `*-client-codegen` directories. `mcp-rust/` either doesn't exist or is empty. Build succeeds. + +If the build fails because the Maven plugin can't be resolved, double-check the `file://` URL has no spaces and that the `pom` files in `smithy/maven-local/` declare the same `groupId:artifactId:version` as the dependency line. + +- [ ] **Step 4: Commit.** + +```bash +git add smithy/smithy-build.json makefile +git commit -m "build: register smithy-mcp-codegen plugin in smithy-build + +Adds the mcp-rust plugin entry and points SMITHY_MAVEN_REPOS at the +bundled local Maven repo. Smithy build is a no-op for MCP output until +@mcpTool annotations are added on operations (next task)." +``` + +--- + +## Task 3: Annotate smithy models with @mcpTool + +**Files:** +- Modify: each `.smithy` file under `smithy/models/` that contains an operation + +**Mechanical operation, applied uniformly:** every operation in the `io.superposition#Superposition` service gets a bare `@mcpTool` annotation. The generator (post PR #5) falls back to `@documentation` for the tool description. + +- [ ] **Step 1: Enumerate the smithy model files to edit.** + +```bash +ls smithy/models/*.smithy +``` + +Expected output: 16 files (audit, common, config, context, default-config, dimension, experiment_config, experiment_groups, experiments, functions, main, organisation, secret, type-templates, variable, webhook, workspace — that's actually 17 entries; some don't contain operations). + +For each file, check whether it contains an `operation` block: + +```bash +grep -l '^operation \|^@http' smithy/models/*.smithy +``` + +The files printed are the ones to edit. + +- [ ] **Step 2: For each operation-bearing file, add the `use` import.** + +Open the file. Below the `namespace io.superposition` line, ensure this line is present (add it if absent): + +```smithy +use software.amazon.smithy.mcp#mcpTool +``` + +If the file already has other `use` lines, group with them in alphabetical order. + +- [ ] **Step 3: For each operation in the file, add `@mcpTool` directly above the existing `@http` annotation.** + +Example diff (config.smithy): + +```diff ++@mcpTool + @documentation("Retrieves the latest config with no processing for high-performance access.") + @http(method: "GET", uri: "/config/fast") + @tags(["Configuration Management"]) + operation GetConfigFast { +``` + +Apply uniformly. There is no scoping decision to make (per spec §4.2: every operation gets the annotation; read-only-only was reconsidered to all-operations during brainstorming). + +- [ ] **Step 4: Run `make smithy-build` and verify the MCP crate is generated.** + +```bash +make smithy-clean-build +ls -la smithy/output/source/mcp-rust/ +``` + +Expected: `smithy/output/source/mcp-rust/` exists and contains at minimum `Cargo.toml`, `src/lib.rs`, `src/tools.rs`, `src/types.rs`, `src/server.rs`. + +- [ ] **Step 5: Sanity-check the tool count against the operation count.** + +```bash +grep -c 'pub fn tool_info_' smithy/output/source/mcp-rust/src/tools.rs +grep -c '^operation ' smithy/models/*.smithy | awk -F: '{sum += $2} END {print sum}' +``` + +Both numbers should match. If they don't, an operation is missing `@mcpTool` — find and fix. + +- [ ] **Step 6: Commit.** + +```bash +git add smithy/models/ +git commit -m "feat(smithy): annotate operations with @mcpTool + +Every operation in the io.superposition#Superposition service is +exposed as an MCP tool. Descriptions come from each operation's +existing @documentation trait via the generator's fallback (see +juspay/smithy-mcp-generator#5)." +``` + +--- + +## Task 4: Add `mcp-rust.patch` and integrate into `make smithy-clients` + +**Files:** +- Create: `smithy/patches/mcp-rust.patch` +- Modify: `makefile` + +- [ ] **Step 1: Inspect the generated `Cargo.toml` to learn its exact pre-patch shape.** + +```bash +cat smithy/output/source/mcp-rust/Cargo.toml +``` + +Note the exact `version = "..."` line and `edition = "..."` line — they'll be in the patch. + +- [ ] **Step 2: Use the existing `smithy/patches/rust.patch` as a template.** + +```bash +cat smithy/patches/rust.patch +``` + +The patch rewrites the SDK's `Cargo.toml` to inherit `version`/`license`/`homepage` from the workspace and adds a `readme` field. Apply the same transformations to the MCP crate. + +- [ ] **Step 3: Create `smithy/patches/mcp-rust.patch`.** + +The diff must be against the file at path `crates/superposition_mcp/Cargo.toml` (the destination path after `make smithy-clients` runs). The exact pre-image will be the contents you read in Step 1 — use those line-for-line so the patch applies cleanly. + +Template (substitute exact pre-image lines from Step 1): + +```diff +diff --git a/crates/superposition_mcp/Cargo.toml b/crates/superposition_mcp/Cargo.toml +--- a/crates/superposition_mcp/Cargo.toml ++++ b/crates/superposition_mcp/Cargo.toml +@@ -1,8 +1,12 @@ + [package] + name = "superposition_mcp" +-version = "0.1.0" ++version.workspace = true + edition = "2021" ++license = { workspace = true } ++homepage = { workspace = true } ++repository = "https://github.com/juspay/superposition" ++readme = "README.md" +``` + +Save the patch. + +- [ ] **Step 4: Extend the `smithy-clients` make target to install the MCP crate.** + +Open `makefile`. Find the `smithy-clients` target (currently at line 266). After the existing block that handles `crates/superposition_sdk` (around line 296 — look for `rm -rf crates/superposition_sdk`), add the MCP crate's analogue immediately after: + +```makefile + rm -rf crates/superposition_mcp + mkdir -p crates/superposition_mcp + cp -r $(SMITHY_BUILD_SRC)/mcp-rust/*\ + crates/superposition_mcp +``` + +The hand-preserved files (`README.md`, `CHANGELOG.md`) don't exist yet on this first run, so don't add `git restore` lines for them yet — they'll be added in Task 5 once the files are committed. + +The trailing `git apply smithy/patches/*.patch` at the end of the target (line 311) will pick up `mcp-rust.patch` automatically. + +- [ ] **Step 5: Run `make smithy-clients` and verify the MCP crate appears under `crates/`.** + +```bash +make smithy-clients +ls crates/superposition_mcp/ +cat crates/superposition_mcp/Cargo.toml +``` + +Expected: directory exists with `Cargo.toml`, `src/`. The Cargo.toml shows `version.workspace = true`, `license = { workspace = true }`, etc. + +If `git apply` fails on `mcp-rust.patch`, the pre-image lines in the patch don't match the generated output — re-do Step 3 using the actual generated text. + +- [ ] **Step 6: Commit.** + +```bash +git add smithy/patches/mcp-rust.patch makefile +git commit -m "build(smithy): install superposition_mcp via smithy-clients + +Patches generated Cargo.toml to inherit workspace fields, mirroring +the superposition_sdk pattern." +``` + +--- + +## Task 5: Add `superposition_mcp` to the workspace + hand-preserved README/CHANGELOG + +**Files:** +- Modify: `Cargo.toml` +- Modify: `makefile` +- Create: `crates/superposition_mcp/README.md` +- Create: `crates/superposition_mcp/CHANGELOG.md` + +- [ ] **Step 1: Add `crates/superposition_mcp` to workspace members.** + +Open `Cargo.toml`. Locate the `members = [` list. Add (alphabetically near `superposition_macros`): + +```toml +"crates/superposition_mcp", +``` + +- [ ] **Step 2: Add `superposition_mcp` to `EXCLUDE_PACKAGES` in the makefile.** + +Open `makefile`. Line 9 currently reads: + +```makefile +EXCLUDE_PACKAGES := experimentation_client_integration_example superposition_sdk +``` + +Change to: + +```makefile +EXCLUDE_PACKAGES := experimentation_client_integration_example superposition_sdk superposition_mcp +``` + +- [ ] **Step 3: Create the hand-preserved `README.md`.** + +```bash +cat > crates/superposition_mcp/README.md <<'EOF' +# superposition_mcp + +Generated Rust crate exposing the `io.superposition#Superposition` smithy service as an MCP (Model Context Protocol) server. + +**Do not edit files in this crate by hand** — they are regenerated by `make smithy-clients` from `smithy/models/*.smithy`. The exceptions are `README.md` and `CHANGELOG.md`, which are hand-preserved and restored after regeneration. + +The deployable binary that wraps this crate lives in `../superposition_mcp_server/`. + +See `docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md` for design context. +EOF +``` + +- [ ] **Step 4: Create the hand-preserved `CHANGELOG.md`.** + +```bash +cat > crates/superposition_mcp/CHANGELOG.md <<'EOF' +# Changelog + +This file is hand-maintained alongside the otherwise auto-generated `superposition_mcp` crate. + +## Unreleased +EOF +``` + +- [ ] **Step 5: Update `smithy-clients` target to `git restore` the preserved files.** + +Open `makefile`. Find the MCP block added in Task 4, Step 4. Insert `git restore` lines between `mkdir -p` and `cp -r`: + +```makefile + rm -rf crates/superposition_mcp + mkdir -p crates/superposition_mcp + git restore crates/superposition_mcp/README.md + git restore crates/superposition_mcp/CHANGELOG.md + cp -r $(SMITHY_BUILD_SRC)/mcp-rust/*\ + crates/superposition_mcp +``` + +Note: `git restore` only works once the files have been committed. They get committed in Step 7 below. + +- [ ] **Step 6: Verify `cargo check` passes against the new workspace member.** + +```bash +cargo check -p superposition_mcp +``` + +Expected: compiles. If it complains about a missing `smithy-mcp-runtime` dep, that's a problem — the generated `Cargo.toml` should already declare it. Inspect: + +```bash +cat crates/superposition_mcp/Cargo.toml +``` + +The generated `Cargo.toml` declares `smithy-mcp-runtime = "0.1.0"`, which doesn't resolve from crates.io because the runtime isn't published. Patch the generated Cargo.toml in `smithy/patches/mcp-rust.patch` to use a git ref instead. Add to the patch: + +```diff +-smithy-mcp-runtime = "0.1.0" ++smithy-mcp-runtime = { git = "https://github.com/juspay/smithy-mcp-generator.git", rev = "", features = ["stdio", "http"] } +``` + +Substitute `` with the commit SHA from the generator repo that includes PR #5 (look up via `cd ../smithy-mcp-generator && git rev-parse HEAD`). + +Re-run: + +```bash +make smithy-clients +cargo check -p superposition_mcp +``` + +Expected: compiles successfully. + +- [ ] **Step 7: Commit.** + +```bash +git add Cargo.toml makefile crates/superposition_mcp/README.md crates/superposition_mcp/CHANGELOG.md smithy/patches/mcp-rust.patch +git commit -m "feat: add superposition_mcp workspace member + +Generated crate from smithy-mcp-generator. Patch redirects +smithy-mcp-runtime to a git ref (it is not yet on crates.io). +README and CHANGELOG are hand-preserved via git-restore in the +smithy-clients make target." +``` + +- [ ] **Step 8: Also commit the regenerated crate body now that the patch is stable.** + +```bash +git add crates/superposition_mcp/ +git status +``` + +Verify no other unrelated files are staged. Then: + +```bash +git commit -m "chore(generated): superposition_mcp crate body + +Generated by smithy-mcp-generator from smithy/models/*.smithy. Re-run +make smithy-clients to regenerate." +``` + +--- + +## Task 6: Scaffold superposition_mcp_server binary crate + +**Files:** +- Create: `crates/superposition_mcp_server/Cargo.toml` +- Create: `crates/superposition_mcp_server/src/main.rs` (stub) +- Modify: `Cargo.toml` (workspace members) + +- [ ] **Step 1: Create the binary crate manifest.** + +```toml +# crates/superposition_mcp_server/Cargo.toml +[package] +name = "superposition_mcp_server" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository = "https://github.com/juspay/superposition" +description = "MCP server binary exposing the superposition API as tools" + +[[bin]] +name = "superposition-mcp" +path = "src/main.rs" + +[dependencies] +superposition_mcp = { path = "../superposition_mcp" } +superposition_sdk = { path = "../superposition_sdk" } +smithy-mcp-runtime = { git = "https://github.com/juspay/smithy-mcp-generator.git", rev = "", features = ["stdio", "http"] } + +aws-smithy-runtime-api = { workspace = true } +aws-smithy-types = { workspace = true } + +anyhow = { workspace = true } +async-trait = "0.1" +clap = { version = "4", features = ["derive", "env"] } +rmcp = { version = "1.2", features = ["server", "transport-streamable-http-server"] } +secrecy = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { version = "1", features = ["full"] } +tower = "0.5" +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +axum = "0.7" +http = "1" +base64 = { workspace = true } + +[dev-dependencies] +wiremock = "0.6" +``` + +Substitute `` with the same commit SHA used in `smithy/patches/mcp-rust.patch`. Verify that `aws-smithy-runtime-api`, `aws-smithy-types`, `anyhow`, `secrecy`, `serde`, `serde_json`, `tracing`, `base64` are all in `[workspace.dependencies]` in the root `Cargo.toml` — they are at the time of writing. + +If `axum` or `tower` versions need to be added to workspace deps for consistency with other crates, do so. Otherwise pin here is fine since this is a leaf binary. + +- [ ] **Step 2: Create a stub `main.rs` so the crate compiles.** + +```rust +// crates/superposition_mcp_server/src/main.rs +fn main() { + println!("superposition-mcp: not yet implemented"); +} +``` + +- [ ] **Step 3: Add to workspace members.** + +Open the root `Cargo.toml`. Add to `members`: + +```toml +"crates/superposition_mcp_server", +``` + +- [ ] **Step 4: Verify the workspace compiles.** + +```bash +cargo check -p superposition_mcp_server +``` + +Expected: compiles. If `smithy-mcp-runtime` git ref fails to resolve, double-check the SHA exists on `juspay/smithy-mcp-generator`. + +- [ ] **Step 5: Commit.** + +```bash +git add Cargo.toml crates/superposition_mcp_server/ +git commit -m "feat: scaffold superposition_mcp_server binary crate + +Stub main; real wiring lands in subsequent commits." +``` + +--- + +## Task 7: `config` module — env-var loading + tests + +**Files:** +- Create: `crates/superposition_mcp_server/src/config.rs` +- Modify: `crates/superposition_mcp_server/src/main.rs` (add `mod config;`) + +- [ ] **Step 1: Write the failing tests first.** + +Create `crates/superposition_mcp_server/src/config.rs`: + +```rust +use secrecy::SecretString; + +#[derive(Debug, Clone, PartialEq)] +pub enum StaticCreds { + Bearer(SecretString), + Basic { user: String, pass: SecretString }, +} + +impl StaticCreds { + pub fn is_bearer(&self) -> bool { + matches!(self, StaticCreds::Bearer(_)) + } + pub fn is_basic(&self) -> bool { + matches!(self, StaticCreds::Basic { .. }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Defaults { + pub workspace_id: Option, + pub org_id: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + pub endpoint: String, + pub creds: Option, + pub defaults: Defaults, +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConfigError { + #[error("SUPERPOSITION_ENDPOINT is required")] + MissingEndpoint, + #[error("provide either SUPERPOSITION_BEARER_TOKEN or SUPERPOSITION_BASIC_USER+SUPERPOSITION_BASIC_PASS, not both")] + ConflictingCreds, + #[error("SUPERPOSITION_BASIC_USER and SUPERPOSITION_BASIC_PASS must be set together")] + IncompleteBasic, + #[error("stdio mode requires credentials (bearer or basic)")] + StdioRequiresCreds, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Mode { + Stdio, + HttpPassthrough, + HttpWithStaticFallback, +} + +pub fn load(mode: Mode, env: &dyn EnvLookup) -> Result { + let endpoint = env.get("SUPERPOSITION_ENDPOINT").ok_or(ConfigError::MissingEndpoint)?; + + let bearer = env.get("SUPERPOSITION_BEARER_TOKEN"); + let basic_user = env.get("SUPERPOSITION_BASIC_USER"); + let basic_pass = env.get("SUPERPOSITION_BASIC_PASS"); + + let creds = match (bearer, basic_user, basic_pass) { + (Some(_), Some(_), _) | (Some(_), _, Some(_)) => return Err(ConfigError::ConflictingCreds), + (Some(b), None, None) => Some(StaticCreds::Bearer(SecretString::new(b.into()))), + (None, Some(u), Some(p)) => Some(StaticCreds::Basic { user: u, pass: SecretString::new(p.into()) }), + (None, Some(_), None) | (None, None, Some(_)) => return Err(ConfigError::IncompleteBasic), + (None, None, None) => None, + }; + + match mode { + Mode::Stdio if creds.is_none() => return Err(ConfigError::StdioRequiresCreds), + _ => {} + } + + Ok(Config { + endpoint, + creds, + defaults: Defaults { + workspace_id: env.get("SUPERPOSITION_WORKSPACE_ID"), + org_id: env.get("SUPERPOSITION_ORG_ID"), + }, + }) +} + +pub trait EnvLookup { + fn get(&self, key: &str) -> Option; +} + +pub struct ProcessEnv; +impl EnvLookup for ProcessEnv { + fn get(&self, key: &str) -> Option { + std::env::var(key).ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct MapEnv(HashMap); + impl EnvLookup for MapEnv { + fn get(&self, key: &str) -> Option { + self.0.get(key).cloned() + } + } + fn env(pairs: &[(&str, &str)]) -> MapEnv { + MapEnv(pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()) + } + + #[test] + fn stdio_requires_endpoint() { + let e = env(&[("SUPERPOSITION_BEARER_TOKEN", "t")]); + assert_eq!(load(Mode::Stdio, &e), Err(ConfigError::MissingEndpoint)); + } + + #[test] + fn stdio_requires_creds() { + let e = env(&[("SUPERPOSITION_ENDPOINT", "https://api.example.com")]); + assert_eq!(load(Mode::Stdio, &e), Err(ConfigError::StdioRequiresCreds)); + } + + #[test] + fn stdio_accepts_bearer() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert!(c.creds.unwrap().is_bearer()); + } + + #[test] + fn stdio_accepts_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BASIC_USER", "u"), + ("SUPERPOSITION_BASIC_PASS", "p"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert!(c.creds.unwrap().is_basic()); + } + + #[test] + fn rejects_both_bearer_and_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ("SUPERPOSITION_BASIC_USER", "u"), + ("SUPERPOSITION_BASIC_PASS", "p"), + ]); + assert_eq!(load(Mode::Stdio, &e), Err(ConfigError::ConflictingCreds)); + } + + #[test] + fn rejects_incomplete_basic() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BASIC_USER", "u"), + ]); + assert_eq!(load(Mode::Stdio, &e), Err(ConfigError::IncompleteBasic)); + } + + #[test] + fn http_passthrough_accepts_no_creds() { + let e = env(&[("SUPERPOSITION_ENDPOINT", "https://api.example.com")]); + let c = load(Mode::HttpPassthrough, &e).unwrap(); + assert!(c.creds.is_none()); + } + + #[test] + fn defaults_populate_from_env() { + let e = env(&[ + ("SUPERPOSITION_ENDPOINT", "https://api.example.com"), + ("SUPERPOSITION_BEARER_TOKEN", "t"), + ("SUPERPOSITION_WORKSPACE_ID", "w1"), + ("SUPERPOSITION_ORG_ID", "o1"), + ]); + let c = load(Mode::Stdio, &e).unwrap(); + assert_eq!(c.defaults.workspace_id.as_deref(), Some("w1")); + assert_eq!(c.defaults.org_id.as_deref(), Some("o1")); + } +} +``` + +Also add `thiserror` to dependencies in `Cargo.toml` if missing: + +```toml +thiserror = { workspace = true } +``` + +(Confirm `thiserror` is in `[workspace.dependencies]`; if absent, add `thiserror = "1"` to this crate's deps instead.) + +- [ ] **Step 2: Register the module in `main.rs`.** + +Replace the stub `main.rs` with: + +```rust +mod config; + +fn main() { + println!("superposition-mcp: not yet implemented"); +} +``` + +- [ ] **Step 3: Run the tests; verify they pass.** + +```bash +cargo test -p superposition_mcp_server config:: +``` + +Expected: 8 tests pass. + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/ +git commit -m "feat(mcp-server): env-var config loading and validation" +``` + +--- + +## Task 8: `auth` module — AuthValue, header parsing, task-local, ResolveIdentity + +**Files:** +- Create: `crates/superposition_mcp_server/src/auth.rs` +- Modify: `crates/superposition_mcp_server/src/main.rs` (`mod auth;`) + +- [ ] **Step 1: Write `auth.rs` with `AuthValue`, header parser, and task-local.** + +```rust +// crates/superposition_mcp_server/src/auth.rs +use base64::Engine; +use secrecy::{ExposeSecret, SecretString}; + +#[derive(Debug, Clone)] +pub enum AuthValue { + Bearer(SecretString), + Basic { user: String, pass: SecretString }, +} + +tokio::task_local! { + pub static SUPERPOSITION_AUTH: AuthValue; +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum AuthParseError { + #[error("missing Authorization header")] + Missing, + #[error("malformed Authorization header")] + Malformed, + #[error("unsupported authentication scheme")] + UnsupportedScheme, +} + +impl AuthValue { + /// Parse an `Authorization` header value. + /// Supports `Bearer ` and `Basic `. + pub fn parse_header(value: Option<&str>) -> Result { + let raw = value.ok_or(AuthParseError::Missing)?.trim(); + let (scheme, rest) = raw.split_once(' ').ok_or(AuthParseError::Malformed)?; + let scheme = scheme.to_ascii_lowercase(); + let rest = rest.trim(); + match scheme.as_str() { + "bearer" => { + if rest.is_empty() { return Err(AuthParseError::Malformed); } + Ok(AuthValue::Bearer(SecretString::new(rest.to_string().into()))) + } + "basic" => { + let decoded = base64::engine::general_purpose::STANDARD + .decode(rest) + .map_err(|_| AuthParseError::Malformed)?; + let decoded = String::from_utf8(decoded).map_err(|_| AuthParseError::Malformed)?; + let (user, pass) = decoded.split_once(':').ok_or(AuthParseError::Malformed)?; + Ok(AuthValue::Basic { + user: user.to_string(), + pass: SecretString::new(pass.to_string().into()), + }) + } + _ => Err(AuthParseError::UnsupportedScheme), + } + } + + /// Bearer-token string (only meaningful for Bearer variant). + pub fn bearer(&self) -> Option<&str> { + match self { + AuthValue::Bearer(t) => Some(t.expose_secret()), + _ => None, + } + } + + /// (user, pass) for Basic variant. + pub fn basic(&self) -> Option<(&str, &str)> { + match self { + AuthValue::Basic { user, pass } => Some((user, pass.expose_secret())), + _ => None, + } + } +} + +impl From for AuthValue { + fn from(c: crate::config::StaticCreds) -> Self { + match c { + crate::config::StaticCreds::Bearer(t) => AuthValue::Bearer(t), + crate::config::StaticCreds::Basic { user, pass } => AuthValue::Basic { user, pass }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_bearer() { + let v = AuthValue::parse_header(Some("Bearer abc123")).unwrap(); + assert_eq!(v.bearer(), Some("abc123")); + } + + #[test] + fn parses_bearer_case_insensitive_scheme() { + let v = AuthValue::parse_header(Some("bearer abc")).unwrap(); + assert_eq!(v.bearer(), Some("abc")); + } + + #[test] + fn parses_basic() { + let creds = base64::engine::general_purpose::STANDARD.encode("alice:s3cret"); + let v = AuthValue::parse_header(Some(&format!("Basic {}", creds))).unwrap(); + assert_eq!(v.basic(), Some(("alice", "s3cret"))); + } + + #[test] + fn rejects_missing() { + assert_eq!(AuthValue::parse_header(None), Err(AuthParseError::Missing)); + } + + #[test] + fn rejects_empty_bearer() { + assert_eq!(AuthValue::parse_header(Some("Bearer ")), Err(AuthParseError::Malformed)); + } + + #[test] + fn rejects_unknown_scheme() { + assert_eq!(AuthValue::parse_header(Some("Digest xyz")), Err(AuthParseError::UnsupportedScheme)); + } + + #[test] + fn rejects_malformed_basic_no_colon() { + let creds = base64::engine::general_purpose::STANDARD.encode("no-colon-here"); + let v = AuthValue::parse_header(Some(&format!("Basic {}", creds))); + assert_eq!(v, Err(AuthParseError::Malformed)); + } +} +``` + +- [ ] **Step 2: Register the module in main.rs.** + +```rust +mod auth; +mod config; + +fn main() { + println!("superposition-mcp: not yet implemented"); +} +``` + +- [ ] **Step 3: Run tests.** + +```bash +cargo test -p superposition_mcp_server auth:: +``` + +Expected: 7 tests pass. + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/ +git commit -m "feat(mcp-server): AuthValue and header parsing" +``` + +--- + +## Task 9: `auth` module continued — smithy-rs `ResolveIdentity` impl + +**Files:** +- Modify: `crates/superposition_mcp_server/src/auth.rs` + +The SDK `Client` is built once at startup with a custom `ResolveIdentity` for bearer and basic schemes. The resolver reads from the `SUPERPOSITION_AUTH` task-local. If the task-local is unset and a static fallback was configured, the resolver returns that fallback's identity instead. + +- [ ] **Step 1: Inspect the smithy-rs identity resolver trait surface in the generated SDK.** + +```bash +grep -rn "ResolveIdentity\|IdentityFuture\|http::Token\|http::Login" crates/superposition_sdk/src/config.rs | head -20 +``` + +Note the relevant types: `aws_smithy_runtime_api::client::identity::http::Token` for bearer; `aws_smithy_runtime_api::client::identity::http::Login` for basic; trait `aws_smithy_runtime_api::client::identity::ResolveIdentity`. + +- [ ] **Step 2: Add the resolver implementations.** + +Append to `auth.rs`: + +```rust +use aws_smithy_runtime_api::client::identity::{ + http::{Login, Token}, + Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver, +}; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig; +use aws_smithy_types::config_bag::ConfigBag; + +/// Resolves bearer-token identity from the task-local, falling back to a static value if provided. +#[derive(Debug)] +pub struct BearerResolver { + pub fallback: Option, +} + +impl ResolveIdentity for BearerResolver { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::ready({ + let token = SUPERPOSITION_AUTH + .try_with(|v| v.bearer().map(|s| s.to_string())) + .ok() + .flatten() + .or_else(|| self.fallback.as_ref().map(|s| s.expose_secret().to_string())); + + match token { + Some(t) => Ok(Identity::new(Token::new(t, None), None)), + None => Err("no bearer credential in task-local or fallback".into()), + } + }) + } +} + +/// Resolves basic-auth identity from the task-local, falling back to a static value if provided. +#[derive(Debug)] +pub struct BasicResolver { + pub fallback: Option<(String, SecretString)>, +} + +impl ResolveIdentity for BasicResolver { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::ready({ + let login = SUPERPOSITION_AUTH + .try_with(|v| v.basic().map(|(u, p)| (u.to_string(), p.to_string()))) + .ok() + .flatten() + .or_else(|| { + self.fallback.as_ref().map(|(u, p)| (u.clone(), p.expose_secret().to_string())) + }); + + match login { + Some((u, p)) => Ok(Identity::new(Login::new(u, p, None), None)), + None => Err("no basic credential in task-local or fallback".into()), + } + }) + } +} + +pub fn shared_bearer(fallback: Option) -> SharedIdentityResolver { + SharedIdentityResolver::new(BearerResolver { fallback }) +} + +pub fn shared_basic(fallback: Option<(String, SecretString)>) -> SharedIdentityResolver { + SharedIdentityResolver::new(BasicResolver { fallback }) +} +``` + +- [ ] **Step 3: Verify it compiles.** + +```bash +cargo check -p superposition_mcp_server +``` + +If unresolved imports surface, double-check the exact type paths against what `crates/superposition_sdk/src/config.rs` re-exports — `Token` is `aws_smithy_runtime_api::client::identity::http::Token` and is re-exported as `crate::config::Token` in the SDK, confirmed at `crates/superposition_sdk/src/config.rs:1125`. + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/src/auth.rs +git commit -m "feat(mcp-server): smithy-rs ResolveIdentity impls for task-local auth" +``` + +--- + +## Task 10: `dispatch` module — default workspace/org injection + +**Files:** +- Create: `crates/superposition_mcp_server/src/dispatch.rs` +- Modify: `crates/superposition_mcp_server/src/main.rs` (`mod dispatch;`) + +- [ ] **Step 1: Write the dispatch wrapper and tests.** + +```rust +// crates/superposition_mcp_server/src/dispatch.rs +use serde_json::{Map, Value}; + +use crate::config::Defaults; + +/// If the params is a JSON object and is missing `workspace_id` / `org_id`, +/// inject defaults from the configuration. Other shapes (non-object) are +/// passed through unchanged — the SDK call will surface a typed deserialize +/// error verbatim. +pub fn inject_defaults(params: Value, defaults: &Defaults) -> Value { + let Value::Object(mut obj) = params else { + return params; + }; + if !obj.contains_key("workspace_id") { + if let Some(w) = defaults.workspace_id.as_ref() { + obj.insert("workspace_id".to_string(), Value::String(w.clone())); + } + } + if !obj.contains_key("org_id") { + if let Some(o) = defaults.org_id.as_ref() { + obj.insert("org_id".to_string(), Value::String(o.clone())); + } + } + Value::Object(obj) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn defaults(w: Option<&str>, o: Option<&str>) -> Defaults { + Defaults { + workspace_id: w.map(String::from), + org_id: o.map(String::from), + } + } + + #[test] + fn injects_both_when_absent() { + let params = json!({"key": "v"}); + let out = inject_defaults(params, &defaults(Some("w"), Some("o"))); + assert_eq!(out, json!({"key": "v", "workspace_id": "w", "org_id": "o"})); + } + + #[test] + fn param_value_wins_over_default() { + let params = json!({"workspace_id": "explicit"}); + let out = inject_defaults(params, &defaults(Some("default"), None)); + assert_eq!(out, json!({"workspace_id": "explicit"})); + } + + #[test] + fn injects_org_only_when_only_org_defaulted() { + let params = json!({}); + let out = inject_defaults(params, &defaults(None, Some("o"))); + assert_eq!(out, json!({"org_id": "o"})); + } + + #[test] + fn no_change_when_no_defaults() { + let params = json!({"x": 1}); + let out = inject_defaults(params.clone(), &defaults(None, None)); + assert_eq!(out, params); + } + + #[test] + fn passes_through_non_object() { + let params = json!(42); + let out = inject_defaults(params.clone(), &defaults(Some("w"), Some("o"))); + assert_eq!(out, params); + } +} +``` + +- [ ] **Step 2: Register the module.** + +```rust +mod auth; +mod config; +mod dispatch; + +fn main() { + println!("superposition-mcp: not yet implemented"); +} +``` + +- [ ] **Step 3: Run tests.** + +```bash +cargo test -p superposition_mcp_server dispatch:: +``` + +Expected: 5 tests pass. + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/ +git commit -m "feat(mcp-server): inject workspace/org defaults into tool params" +``` + +--- + +## Task 11: Wire the SDK Client + Router with the dispatch wrapper + +**Files:** +- Modify: `crates/superposition_mcp_server/src/main.rs` + +The generator's `McpServer::new(client)` registers each tool with a closure that calls `tools::handle_(&client, params)`. We need to wrap each registration so the params pass through `dispatch::inject_defaults` first. The cleanest path is to use `McpServer::new(client).into_router()` and *not* use the generator's pre-built router, because we need to intercept params. Instead, we duplicate the per-tool registration with our wrapper inline. + +For this task, we accept the duplication and write our own builder. (Followup: spec §11.4 notes that the generator could later grow a parameter-interceptor hook, eliminating this duplication.) + +- [ ] **Step 1: Build the customized router with default-injection wrappers.** + +Replace `main.rs` with the following, which: + +1. Parses CLI args with clap. +2. Loads config from env. +3. Builds the SDK `Client` with appropriate identity resolvers. +4. Constructs a router that, for each `@mcpTool` operation, wraps the generated `handle_` with `inject_defaults`. + +Use a build script or generated helper to enumerate tools. **For this plan, we hand-write the registration list.** It can be regenerated mechanically: `grep -o 'pub async fn handle_[a-z_]*' crates/superposition_mcp/src/tools.rs | sort -u` produces the function names; the `tool_info_*` partner has the same suffix. + +```rust +// crates/superposition_mcp_server/src/main.rs +mod auth; +mod config; +mod dispatch; + +use std::sync::Arc; + +use clap::Parser; +use secrecy::ExposeSecret; +use smithy_mcp_runtime::Router; +use superposition_mcp::tools; +use superposition_sdk::Client; +use tracing_subscriber::EnvFilter; + +use crate::auth::{shared_basic, shared_bearer, AuthValue, SUPERPOSITION_AUTH}; +use crate::config::{Config, Defaults, Mode}; + +#[derive(Parser, Debug)] +#[command(name = "superposition-mcp", about = "MCP server for the Superposition API")] +struct Cli { + /// Bind address for HTTP+SSE transport. If unset, stdio is used. + #[arg(long, value_name = "ADDR")] + http: Option, + + /// In HTTP mode, fall back to env-var credentials when no Authorization header is present. + #[arg(long, requires = "http")] + allow_static_auth: bool, +} + +fn init_logging() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(std::io::stderr) // STDOUT is the JSON-RPC channel on stdio + .init(); +} + +fn build_client(cfg: &Config, mode: Mode) -> Client { + use superposition_sdk::config::Builder; + + let mut builder = Builder::new().endpoint_url(&cfg.endpoint); + + // Convert static creds (if any) into fallback values for the resolvers. + let bearer_fallback = match (&cfg.creds, mode) { + (Some(config::StaticCreds::Bearer(t)), Mode::Stdio) => Some(t.clone()), + (Some(config::StaticCreds::Bearer(t)), Mode::HttpWithStaticFallback) => Some(t.clone()), + _ => None, + }; + let basic_fallback = match (&cfg.creds, mode) { + (Some(config::StaticCreds::Basic { user, pass }), Mode::Stdio) => { + Some((user.clone(), pass.clone())) + } + (Some(config::StaticCreds::Basic { user, pass }), Mode::HttpWithStaticFallback) => { + Some((user.clone(), pass.clone())) + } + _ => None, + }; + + let bearer = shared_bearer(bearer_fallback); + let basic = shared_basic(basic_fallback); + + builder = builder + .bearer_token_resolver(bearer) + .basic_auth_login_resolver(basic); + + Client::from_conf(builder.build()) +} + +fn build_router(client: Client, defaults: Defaults) -> Router { + let client = Arc::new(client); + let defaults = Arc::new(defaults); + let mut router = Router::new(); + + // Macro to keep the body of each registration small. + // For every (handle_, tool_info_) pair in superposition_mcp::tools, + // wrap the call with inject_defaults. + macro_rules! register { + ($info_fn:ident, $handle_fn:ident) => {{ + let c = client.clone(); + let d = defaults.clone(); + router.register_tool(tools::$info_fn(), move |params| { + let c = c.clone(); + let d = d.clone(); + async move { + let params = dispatch::inject_defaults(params, &d); + tools::$handle_fn(&c, params).await + } + }); + }}; + } + + // Generated registration list. To regenerate: + // grep -E '^pub async fn handle_' crates/superposition_mcp/src/tools.rs \ + // | awk '{print $4}' | tr -d '(' \ + // | sed 's/^handle_\(.*\)$/ register!(tool_info_\1, handle_\1);/' + // + // PASTE THE OUTPUT OF THE COMMAND ABOVE BELOW. The placeholder line is a + // marker for the implementing engineer — replace with the generated list. + /* BEGIN GENERATED TOOL REGISTRATIONS */ + register!(tool_info_get_config_fast, handle_get_config_fast); + // ... (~85 more lines) + /* END GENERATED TOOL REGISTRATIONS */ + + router +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + init_logging(); + let cli = Cli::parse(); + + let mode = match (&cli.http, cli.allow_static_auth) { + (None, _) => Mode::Stdio, + (Some(_), true) => Mode::HttpWithStaticFallback, + (Some(_), false) => Mode::HttpPassthrough, + }; + + let cfg = config::load(mode, &config::ProcessEnv)?; + let client = build_client(&cfg, mode); + let router = build_router(client, cfg.defaults.clone()); + + match (mode, cli.http.as_deref()) { + (Mode::Stdio, _) => stdio_serve(cfg, router).await, + (Mode::HttpPassthrough, Some(addr)) | (Mode::HttpWithStaticFallback, Some(addr)) => { + // Implemented in Task 12 (transport_http module). + unimplemented!("HTTP transport — see Task 12"); + } + _ => unreachable!(), + } +} + +async fn stdio_serve(cfg: Config, router: Router) -> anyhow::Result<()> { + // In stdio mode, the static credential is fixed for the entire process. + // Wrap the entire serve future in SUPERPOSITION_AUTH.scope so the + // ResolveIdentity implementations can read it. + let auth_value: AuthValue = cfg + .creds + .clone() + .expect("config::load enforces creds presence for stdio mode") + .into(); + + SUPERPOSITION_AUTH + .scope(auth_value, async { + smithy_mcp_runtime::serve_stdio(router) + .await + .map_err(|e| anyhow::anyhow!("stdio transport error: {e}")) + }) + .await +} +``` + +- [ ] **Step 2: Generate the per-tool registration block.** + +```bash +grep -E '^pub async fn handle_' crates/superposition_mcp/src/tools.rs \ + | awk '{print $4}' | tr -d '(' \ + | sed 's/^handle_\(.*\)$/ register!(tool_info_\1, handle_\1);/' +``` + +Paste the output between the `BEGIN GENERATED TOOL REGISTRATIONS` and `END GENERATED TOOL REGISTRATIONS` markers in `build_router`. Remove the comment instructions above the marker. + +- [ ] **Step 3: Verify it compiles.** + +```bash +cargo check -p superposition_mcp_server +``` + +Expected: compiles. Common issues: + +- `bearer_token_resolver` / `basic_auth_login_resolver` method names may differ slightly; check the SDK config builder at `crates/superposition_sdk/src/config.rs:208-230`. Adjust accordingly. +- `Client::from_conf` takes a `Config` (not `Builder`); use `builder.build()`. + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/src/main.rs +git commit -m "feat(mcp-server): stdio transport with default injection + +Hand-rolled router builder wraps each generated tool handler with the +workspace/org default injection. The 85-line tool registration block +is regenerated from crates/superposition_mcp/src/tools.rs by the +script in the comment above register!." +``` + +--- + +## Task 12: HTTP transport — Axum mount with task-local-scoping middleware + +**Files:** +- Create: `crates/superposition_mcp_server/src/transport_http.rs` +- Modify: `crates/superposition_mcp_server/src/main.rs` + +- [ ] **Step 1: Write the HTTP transport module.** + +```rust +// crates/superposition_mcp_server/src/transport_http.rs +use std::net::SocketAddr; +use std::sync::Arc; + +use axum::body::Body; +use axum::extract::Request; +use axum::http::{header, StatusCode}; +use axum::middleware::{self, Next}; +use axum::response::Response; +use axum::Router as AxumRouter; +use rmcp::transport::streamable_http_server::tower::{StreamableHttpServerConfig, StreamableHttpService}; +use rmcp::transport::common::server_side_http::SessionManager; +use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; +use tracing::{error, info, warn}; + +use crate::auth::{AuthValue, SUPERPOSITION_AUTH}; +use crate::config::Mode; + +pub async fn serve( + addr: SocketAddr, + mode: Mode, + static_fallback: Option, + router: smithy_mcp_runtime::Router, +) -> anyhow::Result<()> { + let svc = StreamableHttpService::new( + move || Ok(router.clone()), + LocalSessionManager::default().into(), + StreamableHttpServerConfig::default(), + ); + + let allow_static = matches!(mode, Mode::HttpWithStaticFallback); + let fallback = Arc::new(static_fallback); + + let app = AxumRouter::new() + .nest_service("/mcp", svc) + .layer(middleware::from_fn(move |req: Request, next: Next| { + let fallback = fallback.clone(); + async move { auth_layer(req, next, allow_static, fallback).await } + })); + + info!(%addr, "MCP server listening (HTTP)"); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} + +async fn auth_layer( + req: Request, + next: Next, + allow_static: bool, + fallback: Arc>, +) -> Response { + let header_value = req + .headers() + .get(header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(str::to_owned); + + let auth = match AuthValue::parse_header(header_value.as_deref()) { + Ok(a) => a, + Err(crate::auth::AuthParseError::Missing) if allow_static => match fallback.as_ref() { + Some(a) => { + warn!("falling back to static credentials (--allow-static-auth)"); + a.clone() + } + None => return unauthorized("no Authorization header and no static fallback"), + }, + Err(_) if allow_static && fallback.is_some() => { + // A malformed header explicitly fails — don't silently fall back. + return unauthorized("malformed Authorization header"); + } + Err(e) => return unauthorized(&format!("{}", e)), + }; + + let fut = next.run(req); + SUPERPOSITION_AUTH.scope(auth, fut).await +} + +fn unauthorized(detail: &str) -> Response { + error!("auth rejected: {detail}"); + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(header::WWW_AUTHENTICATE, "Bearer realm=\"superposition-mcp\"") + .body(Body::from(detail.to_string())) + .unwrap() +} +``` + +Some imports above may need adjustment depending on the exact paths rmcp 1.2 exports. If `LocalSessionManager` lives under a slightly different module path, locate it with: + +```bash +cargo doc -p rmcp --no-deps --open +# then search for "LocalSessionManager" +``` + +Or, more cheaply: + +```bash +grep -rn "LocalSessionManager\|StreamableHttpService" "$CARGO_HOME"/registry/src/*rmcp-1.2*/ 2>/dev/null | head -10 +``` + +- [ ] **Step 2: Wire the HTTP path in `main.rs`.** + +Replace the `unimplemented!` block: + +```rust + (Mode::HttpPassthrough, Some(addr)) | (Mode::HttpWithStaticFallback, Some(addr)) => { + let socket_addr: std::net::SocketAddr = addr.parse() + .map_err(|e| anyhow::anyhow!("invalid --http address {addr}: {e}"))?; + let static_fallback: Option = cfg.creds.clone().map(Into::into); + transport_http::serve(socket_addr, mode, static_fallback, router).await + } +``` + +Add `mod transport_http;` to the module list at the top of `main.rs`. + +- [ ] **Step 3: Verify the binary compiles.** + +```bash +cargo check -p superposition_mcp_server +``` + +- [ ] **Step 4: Commit.** + +```bash +git add crates/superposition_mcp_server/ +git commit -m "feat(mcp-server): HTTP+SSE transport with Authorization passthrough + +Mounts rmcp's StreamableHttpService under /mcp on an Axum router. +A tower middleware extracts the inbound Authorization header, parses +it into AuthValue, and wraps the inner service call in +SUPERPOSITION_AUTH.scope so the smithy-rs ResolveIdentity +implementations can read it per-request. --allow-static-auth enables +fallback to env-var creds when the header is absent." +``` + +--- + +## Task 13: Integration test — wiremock-backed end-to-end + +**Files:** +- Create: `crates/superposition_mcp_server/src/lib.rs` +- Create: `crates/superposition_mcp_server/src/build.rs` +- Modify: `crates/superposition_mcp_server/Cargo.toml` +- Modify: `crates/superposition_mcp_server/src/main.rs` +- Create: `crates/superposition_mcp_server/tests/integration.rs` + +Refactor the crate to expose `build_client` / `build_router` from a library target so integration tests can call them directly. + +- [ ] **Step 1: Add `[lib]` and update `[[bin]]` in `Cargo.toml`.** + +Add to `crates/superposition_mcp_server/Cargo.toml` (the `[[bin]]` section already exists from Task 6; add the `[lib]` section just above it): + +```toml +[lib] +name = "superposition_mcp_server" +path = "src/lib.rs" + +[[bin]] +name = "superposition-mcp" +path = "src/main.rs" +``` + +- [ ] **Step 2: Create `src/lib.rs` re-exporting the modules.** + +```rust +// crates/superposition_mcp_server/src/lib.rs +pub mod auth; +pub mod build; +pub mod config; +pub mod dispatch; +``` + +- [ ] **Step 3: Move `build_client` and `build_router` from `main.rs` to a new `src/build.rs`.** + +Cut the `fn build_client(cfg: &Config, mode: Mode) -> Client { ... }` and `fn build_router(client: Client, defaults: Defaults) -> Router { ... }` blocks out of `main.rs` (along with the `register!` macro and the generated registration list inside `build_router`). Paste them into `src/build.rs`. At the top of `build.rs`, change the items from `fn` to `pub fn` and add imports: + +```rust +// crates/superposition_mcp_server/src/build.rs +use std::sync::Arc; + +use smithy_mcp_runtime::Router; +use superposition_mcp::tools; +use superposition_sdk::{config::Builder, Client}; + +use crate::auth::{shared_basic, shared_bearer}; +use crate::config::{Config, Defaults, Mode, StaticCreds}; +use crate::dispatch; + +pub fn build_client(cfg: &Config, mode: Mode) -> Client { + // ... body moved verbatim from main.rs, with `config::StaticCreds` + // replaced by `StaticCreds` (now imported above) ... +} + +pub fn build_router(client: Client, defaults: Defaults) -> Router { + // ... body moved verbatim from main.rs ... +} +``` + +In `main.rs`, remove the module declarations (`mod auth; mod config; ...`) — they now live in `lib.rs`. Replace with a `use` of the library: + +```rust +use superposition_mcp_server::{auth, build, config, dispatch}; + +mod transport_http; // binary-only; never imported by tests +``` + +Update the `main` function so it calls `build::build_client(...)` and `build::build_router(...)` instead of bare names. + +- [ ] **Step 4: Verify the crate still compiles.** + +```bash +cargo check -p superposition_mcp_server +cargo test -p superposition_mcp_server --lib +``` + +Expected: the existing config / auth / dispatch unit tests still pass (they're now in the library target). + +- [ ] **Step 5: Create the integration test.** + +```rust +// crates/superposition_mcp_server/tests/integration.rs +use secrecy::SecretString; +use serde_json::json; +use wiremock::matchers::{header_exists, method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use superposition_mcp_server::auth::{AuthValue, SUPERPOSITION_AUTH}; +use superposition_mcp_server::build::{build_client, build_router}; +use superposition_mcp_server::config::{Config, Defaults, Mode}; + +async fn router_against(endpoint: &str) -> smithy_mcp_runtime::Router { + let cfg = Config { + endpoint: endpoint.to_string(), + creds: None, + defaults: Defaults { workspace_id: None, org_id: None }, + }; + let client = build_client(&cfg, Mode::HttpPassthrough); + build_router(client, cfg.defaults) +} + +fn count_operations_in_smithy() -> usize { + let workspace_root = { + let mut p = std::env::current_dir().unwrap(); + loop { + if p.join("smithy/models").exists() && p.join("Cargo.toml").exists() { + break p; + } + p = p.parent().expect("cargo workspace root").to_path_buf(); + } + }; + let mut count = 0usize; + for entry in std::fs::read_dir(workspace_root.join("smithy/models")).unwrap() { + let path = entry.unwrap().path(); + if path.extension().and_then(|s| s.to_str()) == Some("smithy") { + let body = std::fs::read_to_string(&path).unwrap(); + count += body.matches("@mcpTool").count(); + } + } + count +} + +#[tokio::test] +async fn tools_list_matches_smithy_annotation_count() { + let server = MockServer::start().await; + let router = router_against(&server.uri()).await; + assert_eq!( + router.tool_names().len(), + count_operations_in_smithy(), + "router tool count must equal @mcpTool annotation count in smithy/models" + ); +} + +#[tokio::test] +async fn tool_call_forwards_authorization_header_in_passthrough_mode() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/config/fast")) + .and(header_exists("authorization")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("x-config-version", "1") + .insert_header("last-modified", "Mon, 11 May 2026 12:00:00 GMT") + .set_body_json(json!({"hello": "world"})), + ) + .mount(&server) + .await; + + let router = router_against(&server.uri()).await; + + let auth = AuthValue::Bearer(SecretString::new("tok-abc".to_string().into())); + let result = SUPERPOSITION_AUTH + .scope(auth, async { + router + .test_call_tool("GetConfigFast", json!({"workspace_id": "w", "org_id": "o"})) + .await + }) + .await; + + let value = result.expect("tool call succeeded"); + assert_eq!(value["hello"], json!("world")); + + let received = server.received_requests().await.expect("received_requests"); + let auth_header = received[0] + .headers + .get("authorization") + .expect("authorization header present") + .to_str() + .unwrap(); + assert_eq!(auth_header, "Bearer tok-abc"); +} +``` + +- [ ] **Step 6: Run the integration test.** + +```bash +cargo test -p superposition_mcp_server --test integration +``` + +Expected: both tests pass. If the second test fails because the GetConfigFast operation isn't actually exposed as a tool (rare — would mean the `@mcpTool` annotation was missed on it in Task 3), check `grep '@mcpTool' smithy/models/config.smithy` and fix. + +- [ ] **Step 7: Commit.** + +```bash +git add crates/superposition_mcp_server/ +git commit -m "test(mcp-server): wiremock integration test for tool dispatch and auth passthrough + +Refactors build_client / build_router out of main.rs into a lib target +so the integration test can call them directly without duplicating the +~85-tool registration list." +``` + +--- + +## Task 14: Local smoke test + final verification + +**Files:** none modified + +- [ ] **Step 1: Build the binary in release mode.** + +```bash +cargo build -p superposition_mcp_server --release +ls -la target/release/superposition-mcp +``` + +Expected: binary exists. Note its size for the commit message. + +- [ ] **Step 2: Run against a local superposition instance via stdio.** + +Assumes a local superposition stack is running (`make superposition_dev` or equivalent) and accepts basic auth as `superposition:superposition` (the development default — verify via `keycloak/` config if unsure): + +```bash +SUPERPOSITION_ENDPOINT=http://localhost:8080 \ +SUPERPOSITION_BASIC_USER=superposition \ +SUPERPOSITION_BASIC_PASS=superposition \ +SUPERPOSITION_WORKSPACE_ID=test \ +SUPERPOSITION_ORG_ID=test \ +RUST_LOG=info \ +./target/release/superposition-mcp <<'EOF' +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.0"}}} +{"jsonrpc":"2.0","method":"notifications/initialized"} +{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} +EOF +``` + +Expected: stderr shows INFO logs; stdout shows JSON-RPC responses for `initialize` and `tools/list`. The `tools/list` response contains entries. + +- [ ] **Step 3: Run the same against HTTP transport.** + +```bash +SUPERPOSITION_ENDPOINT=http://localhost:8080 \ +RUST_LOG=info \ +./target/release/superposition-mcp --http 127.0.0.1:8765 & +HTTP_PID=$! + +# In another shell: +curl -s -X POST http://127.0.0.1:8765/mcp \ + -H "Authorization: Basic $(echo -n 'superposition:superposition' | base64)" \ + -H "Accept: application/json, text/event-stream" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.0"}}}' + +kill "$HTTP_PID" +``` + +Expected: HTTP 200 with the initialize response. Without the `Authorization` header, the curl should return HTTP 401. + +- [ ] **Step 4: Run the full workspace test suite to catch regressions.** + +```bash +make lint +make test +``` + +Expected: all green. Lint runs against `superposition_mcp_server` (handwritten) but skips `superposition_mcp` (excluded), matching the SDK pattern. + +- [ ] **Step 5: Verify CI smithy freshness check passes locally.** + +```bash +make smithy-updates +git status +``` + +Expected: no uncommitted changes after the regeneration. If `crates/superposition_mcp/` shows diff, the patch or the committed crate body is out of sync — re-apply. + +- [ ] **Step 6: Final tagging commit.** + +If there is anything left to commit (last lint fix, generated-doc tweak), do it now: + +```bash +git status +git add +git commit -m "chore(mcp-server): final smoke-test cleanup" +``` + +Push the branch: + +```bash +git push -u origin design/mcp-server +``` + +Open the PR via `gh pr create` (don't forget to reference the design spec at `docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md` in the PR body). + +--- + +## Post-implementation follow-ons (not in this plan's scope) + +These are tracked separately, **not blockers** for this plan: + +1. Publish `smithy-mcp-codegen` to a real Maven repo (juspay sandbox or JitPack); remove `smithy/maven-local/` and `file://` from `SMITHY_MAVEN_REPOS` (spec §11.4). +2. Publish `smithy-mcp-runtime` to crates.io; replace git ref in `superposition_mcp/Cargo.toml` and `superposition_mcp_server/Cargo.toml`. +3. Move HTTP+SSE transport wiring (currently in `transport_http.rs`) upstream into `smithy-mcp-runtime` so the generated `McpServer` exposes a `serve_http(addr)` method analogous to `serve_stdio()`. +4. Add a parameter-interceptor hook to the generator so the per-operation `register!` macro in `main.rs` becomes unnecessary (the generator can take the dispatch wrapper as part of its config). +5. In-process embed of the MCP server inside the main superposition Actix binary (spec §12, option (iii) from brainstorming Q1). diff --git a/docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md b/docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md new file mode 100644 index 000000000..290bc8e48 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-superposition-mcp-server-design.md @@ -0,0 +1,381 @@ +# Superposition MCP Server — Design + +**Date:** 2026-05-11 +**Status:** Draft, pending review +**Owner:** Natarajan Kannan + +## 1. Context + +Superposition's smithy models already drive code generation for a Rust SDK and multiple language clients. A new generator — [`juspay/smithy-mcp-generator`](https://github.com/juspay/smithy-mcp-generator) — emits a Rust [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server from the same smithy IDL, exposing each annotated operation as an MCP tool. + +This spec describes how that generated MCP server lands in the superposition repo as a regenerated workspace crate, alongside a hand-written binary that ships as the deployable artifact. The MCP server enables MCP-capable clients (Claude Desktop, Claude Code, mcp-cli, etc.) to call superposition APIs as tools. + +## 2. Architecture overview + +``` +smithy/models/*.smithy smithy/output/source/mcp-rust/ crates/ + (+@mcpTool on each op) (raw generator output) superposition_mcp/ <- generated, regenerable + │ │ │ Cargo.toml, src/{lib,types,tools,server}.rs + │ smithy build (mcp-rust plugin) │ make smithy-clients (copy) │ + ▼ ▼ ───────────────────────────── + ┌──────────────────┐ ┌──────────┐ superposition_mcp_server/ <- hand-written binary + │ ToolGenerator │ ───────────▶ │ JAR │ │ Cargo.toml, src/{main,config,auth,dispatch,transport_http}.rs + │ ServerGenerator │ │ output │ │ + │ StructureGen ... │ └──────────┘ ▼ + └──────────────────┘ ┌─────────────────────┐ + │ superposition-mcp │ <- binary + │ (stdio | http) │ + └─────────────────────┘ + │ + ▼ + superposition HTTP API +``` + +The MCP server runs as a sidecar process. Two transports are supported: stdio (single-tenant, local) and HTTP+SSE (multi-tenant, passthrough-auth). The generated crate is treated as a regenerable artifact, identical in spirit to `crates/superposition_sdk`. Embedding the MCP server in-process inside the main superposition binary is **deferred** (see §12). + +## 3. Crate layout + +Two new workspace members. + +### 3.1 `crates/superposition_mcp` — generated library + +Pure generator output. Overwritten on every `make smithy-clients` run. + +``` +crates/superposition_mcp/ +├── Cargo.toml (generated; patched to inherit version/license/homepage from workspace) +├── README.md (hand-preserved; `git restore` after smithy-clients) +├── CHANGELOG.md (hand-preserved) +└── src/ + ├── lib.rs (re-exports) + ├── types.rs (input/output structs) + ├── tools.rs (bridge fns: one `handle_(&Client, params) -> Result` per @mcpTool op) + └── server.rs (McpServer with tool registry) +``` + +Excluded from `cargo fmt`/`clippy` via the `EXCLUDE_PACKAGES` list in the makefile, matching `superposition_sdk` precedent. + +### 3.2 `crates/superposition_mcp_server` — hand-written binary + +Normal handwritten code; subject to standard fmt/lint/test. + +``` +crates/superposition_mcp_server/ +├── Cargo.toml +└── src/ + ├── main.rs (CLI parsing via clap; transport selection; signal handling) + ├── config.rs (env-var loading; validation; default workspace/org) + ├── auth.rs (AuthValue enum; task-local; static + passthrough ResolveIdentity impls) + ├── dispatch.rs (wrapper over superposition_mcp::server that injects default workspace/org) + └── transport_http.rs (Axum mount for rmcp HTTP+SSE; Authorization-header middleware) +``` + +Produces a single binary, `superposition-mcp`. + +## 4. Smithy model changes + +Three concrete edits. + +### 4.1 Trait import + +Smithy's `use` statement is file-scoped. Add `use software.amazon.smithy.mcp#mcpTool` to every `smithy/models/*.smithy` file that contains an operation receiving `@mcpTool` (i.e., every operation-bearing file). + +### 4.2 Per-operation annotation + +**Scope: all operations in the `io.superposition#Superposition` service.** Every operation gets a bare `@mcpTool` annotation. The generator (after PR [juspay/smithy-mcp-generator#5](https://github.com/juspay/smithy-mcp-generator/pull/5)) falls back to each operation's existing `@documentation` trait for the tool description; no description text needs to be restated. + +Example diff: + +```diff ++@mcpTool + @documentation("Retrieves the latest config with no processing for high-performance access.") + @http(method: "GET", uri: "/config/fast") + @tags(["Configuration Management"]) + operation GetConfigFast { +``` + +This is mechanical: one new line per operation across approximately 16 files under `smithy/models/`. + +### 4.3 No new operations + +This work adds zero new operations. Only annotates existing ones. + +## 5. Build & CI integration + +### 5.1 `smithy/smithy-build.json` + +Add an `mcp-rust` plugin block alongside the existing `rust-client-codegen`: + +```jsonc +"mcp-rust": { + "service": "io.superposition#Superposition", + "package": "superposition_mcp", + "clientSdk": "smithy-rs", + "clientCrate": "superposition_sdk", + "runtimeVersion": "0.1.0" +} +``` + +Add the codegen Maven dependency to the `maven.dependencies` array: + +``` +"in.juspay.smithy:smithy-mcp-codegen:0.1.0", +"in.juspay.smithy:smithy-mcp-traits:0.1.0" +``` + +### 5.2 Local Maven repo for the generator JAR (transitional) + +The codegen JAR is sourced from a **bundled local Maven repository** under `smithy/maven-local/`, in standard Maven layout: + +``` +smithy/maven-local/ +└── in/juspay/smithy/ + ├── smithy-mcp-codegen/0.1.0/ + │ ├── smithy-mcp-codegen-0.1.0.jar + │ └── smithy-mcp-codegen-0.1.0.pom + └── smithy-mcp-traits/0.1.0/ + ├── smithy-mcp-traits-0.1.0.jar + └── smithy-mcp-traits-0.1.0.pom +``` + +The makefile's `SMITHY_MAVEN_REPOS` is extended to include `file://$(CURDIR)/smithy/maven-local`. This is **explicitly transitional** — see §11 for the migration path to a proper Maven repository. + +### 5.3 `Cargo.toml` workspace members + +Add to `members`: + +```toml +"crates/superposition_mcp", +"crates/superposition_mcp_server", +``` + +Add `superposition_mcp` to `EXCLUDE_PACKAGES` in the makefile (alongside `superposition_sdk`). `superposition_mcp_server` is **not** excluded — it's handwritten and gets normal lint/fmt treatment. + +### 5.4 `smithy/patches/mcp-rust.patch` + +Following the `superposition_sdk` precedent (`smithy/patches/rust.patch`), a small patch rewrites the generated `crates/superposition_mcp/Cargo.toml` to inherit `version`/`license`/`homepage` from the workspace. Applied automatically by `make smithy-clients`. + +### 5.5 Makefile `smithy-clients` extension + +```makefile +rm -rf crates/superposition_mcp +mkdir -p crates/superposition_mcp +git restore crates/superposition_mcp/README.md +git restore crates/superposition_mcp/CHANGELOG.md +cp -r $(SMITHY_BUILD_SRC)/mcp-rust/* crates/superposition_mcp +# git apply smithy/patches/*.patch already runs at end of target +``` + +### 5.6 CI freshness + +No new CI work required. The existing `smithy-sdk-generation-check` job at `.github/workflows/ci_check_pr.yaml:391` already: + +1. Installs the Smithy CLI. +2. Runs `make smithy-updates` (which calls `smithy-clients`). +3. Fails on any `git diff` after the run. + +Once the MCP crate is wired into `smithy-clients`, drift between `smithy/models/*.smithy` and `crates/superposition_mcp/` is automatically caught. + +The local Maven repo at `smithy/maven-local/` is checked into the repo (transitionally), so CI doesn't need to fetch the codegen JAR from anywhere external. + +## 6. Binary CLI surface + +``` +superposition-mcp # stdio (default) +superposition-mcp --http 0.0.0.0:8765 # HTTP+SSE on all interfaces +superposition-mcp --http :8765 --allow-static-auth # HTTP, fall back to env creds when Authorization missing +``` + +### 6.1 Environment variables + +| Variable | Required when | Notes | +|---|---|---| +| `SUPERPOSITION_ENDPOINT` | always | Base URL of the superposition HTTP API. | +| `SUPERPOSITION_BEARER_TOKEN` | stdio mode (if no basic) | Bearer-auth credential. Mutually exclusive with basic. | +| `SUPERPOSITION_BASIC_USER` + `SUPERPOSITION_BASIC_PASS` | stdio mode (if no bearer) | Basic-auth credential. Both must be set together. | +| `SUPERPOSITION_WORKSPACE_ID` | optional | Default `workspace_id` injected into tool calls that omit it. | +| `SUPERPOSITION_ORG_ID` | optional | Default `org_id` injected into tool calls that omit it. | +| `RUST_LOG` | optional | Standard `tracing-subscriber` filter. Default `info`. | + +### 6.2 Startup validation + +Validation runs before any MCP handshake. Failures exit non-zero with a clear stderr message: + +- **stdio mode:** endpoint required; exactly one of bearer or basic required. +- **HTTP mode (default):** endpoint required; credentials must be **absent** (passthrough only). If both `SUPERPOSITION_BEARER_TOKEN` and HTTP mode are set without `--allow-static-auth`, fail with a clear message ("static credentials are ignored in HTTP passthrough mode; pass `--allow-static-auth` to enable fallback"). +- **HTTP mode + `--allow-static-auth`:** endpoint required; credentials optional (used only when `Authorization` is absent on a request). + +### 6.3 Logging + +- `tracing-subscriber` initialised once at startup. +- **Writer hardcoded to `stderr`**, regardless of transport. Stdout is the JSON-RPC channel on stdio and any stray write corrupts the protocol. +- Standard span fields on every tool invocation: `tool_name`, `request_id` (from rmcp), and in HTTP mode `remote_addr`. +- **Never log credential values, even truncated.** Never log full request bodies — they may contain customer configuration payloads. +- One INFO log per tool dispatch: name, duration, outcome (success/error). One WARN log on auth-fallback when `--allow-static-auth` actually fires. + +## 7. Auth model + +### 7.1 Static mode (stdio, or HTTP with `--allow-static-auth` fallback) + +At startup, build a single SDK `Client` with `.bearer_token(...)` or `.basic_auth_login(...)` derived from env. The same `Client` is reused across all tool calls. No per-request work. + +### 7.2 Passthrough mode (HTTP default) + +The binary defines: + +```rust +tokio::task_local! { + static SUPERPOSITION_AUTH: AuthValue; +} + +enum AuthValue { + Bearer(SecretString), + Basic { user: String, pass: SecretString }, +} +``` + +A custom smithy-rs `ResolveIdentity` implementation for both `HTTP_BEARER_AUTH_SCHEME_ID` and `HTTP_BASIC_AUTH_SCHEME_ID` reads from this task-local on each SDK call. If the task-local is unset (no `Authorization` header) and `--allow-static-auth` is not enabled, the resolver returns an identity error which propagates as an MCP `AUTH_REQUIRED` error (and HTTP 401 on the transport). + +One SDK `Client` is built at startup with both resolvers wired and is `Arc`-shared across the Axum app state. This preserves connection pooling and HTTP/2 multiplexing across all MCP clients. + +Axum middleware at the MCP route extracts the inbound `Authorization` header, parses it into an `AuthValue`, and wraps the rmcp request handler: + +```rust +SUPERPOSITION_AUTH.scope(auth_value, async move { handler.run().await }) +``` + +No per-request `Client` rebuild. + +### 7.3 Mixed mode (`--allow-static-auth`) + +- **Authorization header present:** behave as passthrough. +- **Authorization header absent:** task-local stays unset; the resolver falls back to env-loaded identity (captured at startup). Logged at WARN once per process (not per request). +- Intended for local development; help text documents this. + +### 7.4 Secrets hygiene + +- All credential values held in `secrecy::SecretString` (already a workspace dependency). +- No credential value crosses a log boundary, even truncated. +- Auth-error messages: `"missing or invalid Authorization header"` / `"basic header parse failed"`. **Never** `"token X is invalid"` or any value-bearing message. + +## 8. Default workspace/org injection + +Many superposition operations take `workspace_id` / `org_id` as input via the `WorkspaceMixin`. The generated MCP tools expose them as required tool parameters — meaning Claude must fill them on every call. + +The binary's `dispatch::dispatch_tool(name, params)` wraps the generated `superposition_mcp::server::McpServer::handle_*` functions. Before delegating, it inspects the params JSON: + +- If `workspace_id` is absent **and** `SUPERPOSITION_WORKSPACE_ID` is set, inject it. +- Same for `org_id` / `SUPERPOSITION_ORG_ID`. +- If both are absent (no param, no env default), pass through unchanged. The SDK call surfaces superposition's error verbatim; we don't second-guess. + +This is **single-tenant convenience only**. In HTTP passthrough mode, env defaults are still allowed (a hosted MCP server can pre-configure a default workspace for its tenants), but the recommended usage is that MCP clients supply per-call. + +No generator change required. + +## 9. Error mapping + +The generator's default mapping already produces `Result<_, McpError>` from each tool bridge. The binary adds two thin layers: + +- **Authentication failures from passthrough mode:** MCP error code `-32001` (server-defined: `AUTH_REQUIRED`), message `"missing or invalid Authorization header"`. On HTTP transport, the Axum middleware also surfaces this as a `401`. +- **SDK errors from superposition (4xx/5xx):** propagated verbatim. The smithy-rs `SdkError` carries the upstream status, code, and message; these are forwarded into the MCP error payload without rewording. Claude sees the same error text a CLI user would. +- **Local errors (panic, poisoned state):** rmcp's panic catch converts to a generic MCP error; logged at `error!` to stderr. + +## 10. Testing strategy + +Three layers, scaled to what each can usefully prove. + +### 10.1 Generated-crate compile check + +Covered by the existing `smithy-sdk-generation-check` CI job once the MCP crate is wired in. If the generator emits code and the workspace compiles, the generator-produced code is sound. **No new test required.** + +### 10.2 Binary unit tests + +In `crates/superposition_mcp_server/src/`: + +- `config::load_from_env` — table-driven test of valid/invalid env combinations (bearer-only, basic-only, both-set rejected, missing endpoint rejected, HTTP mode with static creds without `--allow-static-auth` rejected). +- `dispatch::inject_defaults` — given a JSON params blob and env defaults, asserts the merged blob (param wins, env fills hole, neither present, malformed JSON). +- `auth::AuthValue::parse_header` — header parsing (Bearer, Basic, malformed, empty). + +### 10.3 Integration test + +One test in `crates/superposition_mcp_server/tests/`: + +- Spin up `wiremock` impersonating superposition; records hits, returns canned JSON. +- Build an `McpServer` against an SDK `Client` pointed at it. +- Invoke `tools/list` and a representative `tools/call` (e.g. `GetConfigFast`) via an in-process rmcp client over an in-memory transport. +- Assertions: `tools/list` returns one entry per `@mcpTool`-annotated operation (count derived dynamically from the generated `superposition_mcp::server::McpServer::tools()` to avoid hardcoding a number); `tools/call` returns expected JSON; wiremock recorded a request bearing the expected `Authorization` header (proving passthrough wiring). + +No live-superposition test in this suite. The existing integration suite can add an MCP smoke later if useful. + +## 11. Dependency sourcing & prerequisites + +External artifacts this work depends on, in publish order: + +### 11.1 Generator changes (PR open) + +[juspay/smithy-mcp-generator#5](https://github.com/juspay/smithy-mcp-generator/pull/5) — adds the `@documentation` fallback so bare `@mcpTool` annotations work. **Must merge before this work can land.** + +### 11.2 `smithy-mcp-codegen` and `smithy-mcp-traits` JARs (bundled, transitional) + +Built locally from `juspay/smithy-mcp-generator` at the tag that includes PR #5. Bundled under `smithy/maven-local/` in standard Maven layout. **Both JAR and POM** files for each artifact are required (smithy's Maven resolver walks POMs for transitive deps). + +The `smithy/maven-local/` tree is checked into the superposition repo so CI doesn't need to fetch anything external. + +**Build recipe** (documented in `smithy/maven-local/README.md`): + +```bash +cd /path/to/smithy-mcp-generator +git checkout +./gradlew publishToMavenLocal +cp -r ~/.m2/repository/in/juspay/smithy/{smithy-mcp-codegen,smithy-mcp-traits} \ + /path/to/superposition/smithy/maven-local/in/juspay/smithy/ +``` + +The implementation may need to add a `maven-publish` Gradle plugin block to `smithy-mcp-generator` if it doesn't already configure `publishToMavenLocal`. This is an upstream prerequisite tracked separately. + +### 11.3 `smithy-mcp-runtime` Cargo dependency + +Sourced via **git ref** in `crates/superposition_mcp/Cargo.toml`: + +```toml +[dependencies.smithy-mcp-runtime] +git = "https://github.com/juspay/smithy-mcp-generator.git" +rev = "" +features = ["stdio", "http"] +``` + +The git ref is pinned to the same commit as the codegen JAR's source build, ensuring the runtime's Rust API matches the generated code. + +### 11.4 Migration path to a proper Maven repo + +The bundled-JAR approach in §5.2 is transitional. A follow-on task — **not blocked on this work** — should migrate to one of: + +- **(B) juspay sandbox Maven** (`https://sandbox.assets.juspay.in/smithy/m2`, already in `SMITHY_MAVEN_REPOS`): one `gradle publish` step in the generator repo's release CI, and the bundled JAR + `file://` repo entry are removed. +- **(A) JitPack**: tag the generator repo, add `https://jitpack.io` to `SMITHY_MAVEN_REPOS`, done. Caveat: first-tag latency, third-party availability. + +This spec leaves the choice between (A) and (B) to the follow-on. Tracking issue should reference this section. + +## 12. Out of scope / deferred + +The following are intentionally **not** in this work: + +- **In-process embedding** of the MCP server inside the main superposition binary. Reachable later via the generator's `clientSdk: "custom"` `ToolHandler` mode; would expose `/mcp` on the same Actix port. Requires a separate codegen invocation and a meaningful glue layer (one trait impl per operation). Revisit when there's pull from a hosted superposition deployment. +- **MCP-layer authentication** (OAuth 2.1 token swap, etc.). Passthrough is the only auth model in v1. Hosted scenarios that need an identity store for MCP users come later. +- **Tool-level rate limiting in the MCP binary.** Superposition's existing API-layer limits apply. +- **MCP-aware audit log** (distinct from superposition's existing audit). Tool calls appear in the existing audit as normal API calls. An optional `X-Mcp-Client-Id` header injected by the binary is a possible follow-on. +- **Sub-command surface** (`superposition-mcp doctor`, `superposition-mcp tools list --offline`, etc.). Plain `superposition-mcp` is enough for v1. + +## 13. Risks & open questions + +- **Generator output may not exactly match what `smithy-clients` expects.** The first integration may surface generator-emitted file layouts that the makefile's `cp -r` glob doesn't catch (e.g., hidden files, additional subdirectories). Mitigation: the implementation plan starts by running `make smithy-build` against the model-only changes and inspecting `smithy/output/source/mcp-rust/` before any makefile edits. +- **~85 model edits is enough that a typo will hide.** Mitigation: the implementation plan batches edits per-file under `smithy/models/`, with `smithy build` validation between batches. +- **`smithy-mcp-runtime` git-ref dep means superposition's `Cargo.lock` carries a non-crates.io entry.** PR reviewers will notice. Spec acknowledges this and §11.4 commits to migrating off. +- **Bundled JAR adds binary blobs to the superposition repo.** Smithy build plugin and trait JARs are typically in the low-hundreds-of-KB range, but the exact size will only be known when the JARs are built; the implementation plan re-evaluates if either exceeds ~5MB. Acceptable as a transitional state; tracked for removal in §11.4. +- **`@documentation` text used as `@mcpTool` description.** Some documentation text may be too detailed or HTTP-specific for an MCP tool description (which Claude reads to pick tools). If any operations produce poor tool UX with their documentation-as-description, the fix is to add an explicit `@mcpTool(description: "...")` on a case-by-case basis. The fallback semantics in the generator (PR #5) support both. + +## 14. Implementation prerequisites (order) + +1. PR [juspay/smithy-mcp-generator#5](https://github.com/juspay/smithy-mcp-generator/pull/5) merges. +2. Generator repo gets a `maven-publish` configuration if absent (small upstream task; can be in the same PR or a follow-on). +3. Generator repo gets a tag at the merged-PR commit; JARs and runtime sources are pinned to that tag. +4. In superposition: model annotations + smithy-build.json plugin block + bundled JAR + makefile wiring + binary crate land together as one PR (after step 3). diff --git a/makefile b/makefile index 9ef2f240c..8ff6597c3 100644 --- a/makefile +++ b/makefile @@ -6,7 +6,7 @@ SHELL := /usr/bin/env bash FEATURES ?= ssr FE_FEATURES ?= hydrate CARGO_FLAGS := --color always --no-default-features -EXCLUDE_PACKAGES := experimentation_client_integration_example superposition_sdk +EXCLUDE_PACKAGES := experimentation_client_integration_example superposition_sdk superposition_mcp FMT_EXCLUDE_PACKAGES_REGEX := $(shell echo "$(EXCLUDE_PACKAGES)" | sed "s/ /|/g") LINT_FLAGS := --workspace --all-targets --all-features $(addprefix --exclude ,$(EXCLUDE_PACKAGES)) --no-deps COMPONENT_NAME_FLAGS := @@ -49,7 +49,7 @@ DB_CONTAINER_NAME = $(shell $(call read-container-name,postgres)) DB_UP = $(shell $(call check-container,$(DB_CONTAINER_NAME))) LSTACK_CONTAINER_NAME = $(shell $(call read-container-name,localstack)) LSTACK_UP = $(shell $(call check-container,$(LSTACK_CONTAINER_NAME))) -export SMITHY_MAVEN_REPOS = https://repo1.maven.org/maven2|https://sandbox.assets.juspay.in/smithy/m2 +export SMITHY_MAVEN_REPOS = https://repo1.maven.org/maven2|https://sandbox.assets.juspay.in/smithy/m2|file://$(CURDIR)/smithy/maven-local .PHONY: amend \ amend-no-edit \ @@ -299,6 +299,13 @@ smithy-clients: smithy-build cp -r $(SMITHY_BUILD_SRC)/rust-client-codegen/*\ crates/superposition_sdk + rm -rf crates/superposition_mcp + mkdir -p crates/superposition_mcp + git restore crates/superposition_mcp/README.md + git restore crates/superposition_mcp/CHANGELOG.md + cp -r $(SMITHY_BUILD_SRC)/mcp-rust/*\ + crates/superposition_mcp + @for d in $(SMITHY_BUILD_SRC)/*-client-codegen; do \ [ -d "$$d" ] || continue; \ [[ "$$d" =~ "java" || "$$d" =~ "haskell" || "$$d" =~ "python" || "$$d" =~ "typescript" || "$$d" =~ "rust" ]] && continue; \ diff --git a/smithy/maven-local/README.md b/smithy/maven-local/README.md new file mode 100644 index 000000000..ed990b83e --- /dev/null +++ b/smithy/maven-local/README.md @@ -0,0 +1,28 @@ +# smithy/maven-local + +Bundled local Maven repository for `smithy-mcp-codegen` and `smithy-mcp-traits`. + +## Why this is here + +The smithy-mcp-generator does not yet publish its artifacts to a public Maven repository (and GitHub Packages requires authentication for Maven, even for public repos). To unblock superposition's MCP server work without setting up new publishing infrastructure, the generator JARs are bundled here in standard Maven layout. `SMITHY_MAVEN_REPOS` in the makefile points at this directory via a `file://` URL. + +## How to refresh + +When the generator is updated and a new version needs to be vendored: + +```bash +cd ../smithy-mcp-generator +git checkout +./gradlew :smithy-mcp-traits:publishToMavenLocal :smithy-mcp-codegen:publishToMavenLocal + +cd ../superposition +rm -rf smithy/maven-local/in/juspay/smithy/{smithy-mcp-codegen,smithy-mcp-traits} +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-codegen smithy/maven-local/in/juspay/smithy/ +cp -r ~/.m2/repository/in/juspay/smithy/smithy-mcp-traits smithy/maven-local/in/juspay/smithy/ +``` + +Bump `runtimeVersion` in `smithy/smithy-build.json` to match. + +## Migration plan + +This is transitional. See spec §11.4 — eventual home is either the juspay sandbox Maven repo (already in `SMITHY_MAVEN_REPOS`) or JitPack. When that lands, delete this directory and remove the `file://` entry from `SMITHY_MAVEN_REPOS`. diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.jar b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.jar new file mode 100644 index 000000000..dca2f49c2 Binary files /dev/null and b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.jar differ diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.module b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.module new file mode 100644 index 000000000..14ffde08a --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.module @@ -0,0 +1,83 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "in.juspay.smithy", + "module": "smithy-mcp-codegen", + "version": "0.1.0", + "attributes": { + "org.gradle.status": "release" + } + }, + "createdBy": { + "gradle": { + "version": "8.5" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 11, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "files": [ + { + "name": "smithy-mcp-codegen-0.1.0.jar", + "url": "smithy-mcp-codegen-0.1.0.jar", + "size": 33647, + "sha512": "424fa15f124165111aa7e52d61aa8c3483f55c32557cae8692f80e7036ee1cfd238d9f06c545614a8eb16deec5b82695294d42918ee73b798541fbb06c85ffe1", + "sha256": "d273d534a28fd79071c412bbeab7f9babc43797b423a0d513698c65613275ebb", + "sha1": "98575cbafb2c9d3ac6d0424efd836f05ebe27dd1", + "md5": "f9c75284d9c6c2deadd234a4b4c55707" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 11, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "dependencies": [ + { + "group": "in.juspay.smithy", + "module": "smithy-mcp-traits", + "version": { + "requires": "0.1.0" + } + }, + { + "group": "software.amazon.smithy", + "module": "smithy-model", + "version": { + "requires": "1.55.0" + } + }, + { + "group": "software.amazon.smithy", + "module": "smithy-build", + "version": { + "requires": "1.55.0" + } + } + ], + "files": [ + { + "name": "smithy-mcp-codegen-0.1.0.jar", + "url": "smithy-mcp-codegen-0.1.0.jar", + "size": 33647, + "sha512": "424fa15f124165111aa7e52d61aa8c3483f55c32557cae8692f80e7036ee1cfd238d9f06c545614a8eb16deec5b82695294d42918ee73b798541fbb06c85ffe1", + "sha256": "d273d534a28fd79071c412bbeab7f9babc43797b423a0d513698c65613275ebb", + "sha1": "98575cbafb2c9d3ac6d0424efd836f05ebe27dd1", + "md5": "f9c75284d9c6c2deadd234a4b4c55707" + } + ] + } + ] +} diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.pom b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.pom new file mode 100644 index 000000000..879254309 --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/0.1.0/smithy-mcp-codegen-0.1.0.pom @@ -0,0 +1,33 @@ + + + + + + + + 4.0.0 + in.juspay.smithy + smithy-mcp-codegen + 0.1.0 + + + in.juspay.smithy + smithy-mcp-traits + 0.1.0 + runtime + + + software.amazon.smithy + smithy-model + 1.55.0 + runtime + + + software.amazon.smithy + smithy-build + 1.55.0 + runtime + + + diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/maven-metadata-local.xml b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/maven-metadata-local.xml new file mode 100644 index 000000000..5d9095924 --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-codegen/maven-metadata-local.xml @@ -0,0 +1,13 @@ + + + in.juspay.smithy + smithy-mcp-codegen + + 0.1.0 + 0.1.0 + + 0.1.0 + + 20260512135405 + + diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.jar b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.jar new file mode 100644 index 000000000..5b1bd8f61 Binary files /dev/null and b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.jar differ diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.module b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.module new file mode 100644 index 000000000..ca3f8d57a --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.module @@ -0,0 +1,78 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "in.juspay.smithy", + "module": "smithy-mcp-traits", + "version": "0.1.0", + "attributes": { + "org.gradle.status": "release" + } + }, + "createdBy": { + "gradle": { + "version": "8.5" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 11, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "dependencies": [ + { + "group": "software.amazon.smithy", + "module": "smithy-model", + "version": { + "requires": "1.55.0" + } + } + ], + "files": [ + { + "name": "smithy-mcp-traits-0.1.0.jar", + "url": "smithy-mcp-traits-0.1.0.jar", + "size": 5004, + "sha512": "10382045502386ffa0218a86da4fa55b56b25c54e5c9980b44725ccbf1c97c78f631c13ea392440fd60efc4fbe8f0a0d3aaba5aabb95ba630365407fc5fe4f2c", + "sha256": "884bdaedfd3fb498f3aea0536f2e402340ca8d71651ec1332a15b4650bdd9c8c", + "sha1": "de7dc2bbc2be60af357c543b45d69357b26b387b", + "md5": "7792079a22cd8a51140a92b2b5f5e0d0" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 11, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "dependencies": [ + { + "group": "software.amazon.smithy", + "module": "smithy-model", + "version": { + "requires": "1.55.0" + } + } + ], + "files": [ + { + "name": "smithy-mcp-traits-0.1.0.jar", + "url": "smithy-mcp-traits-0.1.0.jar", + "size": 5004, + "sha512": "10382045502386ffa0218a86da4fa55b56b25c54e5c9980b44725ccbf1c97c78f631c13ea392440fd60efc4fbe8f0a0d3aaba5aabb95ba630365407fc5fe4f2c", + "sha256": "884bdaedfd3fb498f3aea0536f2e402340ca8d71651ec1332a15b4650bdd9c8c", + "sha1": "de7dc2bbc2be60af357c543b45d69357b26b387b", + "md5": "7792079a22cd8a51140a92b2b5f5e0d0" + } + ] + } + ] +} diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.pom b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.pom new file mode 100644 index 000000000..422307b5d --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/0.1.0/smithy-mcp-traits-0.1.0.pom @@ -0,0 +1,21 @@ + + + + + + + + 4.0.0 + in.juspay.smithy + smithy-mcp-traits + 0.1.0 + + + software.amazon.smithy + smithy-model + 1.55.0 + compile + + + diff --git a/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/maven-metadata-local.xml b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/maven-metadata-local.xml new file mode 100644 index 000000000..337f62637 --- /dev/null +++ b/smithy/maven-local/in/juspay/smithy/smithy-mcp-traits/maven-metadata-local.xml @@ -0,0 +1,13 @@ + + + in.juspay.smithy + smithy-mcp-traits + + 0.1.0 + 0.1.0 + + 0.1.0 + + 20260512135405 + + diff --git a/smithy/models/audit.smithy b/smithy/models/audit.smithy index 805290c4c..f8b5f1520 100644 --- a/smithy/models/audit.smithy +++ b/smithy/models/audit.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource AuditLog { identifiers: { id: String @@ -53,6 +55,7 @@ list AuditActionList { member: AuditAction } +@mcpTool @documentation("Retrieves a paginated list of audit logs with support for filtering by date range, table names, actions, and usernames for compliance and monitoring purposes.") @readonly @http(method: "GET", uri: "/audit") diff --git a/smithy/models/config.smithy b/smithy/models/config.smithy index e00935ea8..a4104127a 100644 --- a/smithy/models/config.smithy +++ b/smithy/models/config.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Config { identifiers: { workspace_id: String @@ -90,6 +92,7 @@ structure ConfigData { dimensions: DimensionData } +@mcpTool @documentation("Retrieves config data with context evaluation, including applicable contexts, overrides, and default values based on provided conditions.") @http(method: "POST", uri: "/config") @tags(["Configuration Management"]) @@ -135,6 +138,7 @@ operation GetConfig { } } +@mcpTool @documentation("Retrieves the full config in TOML format, including default configs with schemas, dimensions, and overrides. This endpoint is optimized for clients that prefer TOML format for configuration management.") @readonly @http(method: "POST", uri: "/config/toml") @@ -158,6 +162,7 @@ operation GetConfigToml { } } +@mcpTool @documentation("Retrieves the full config in JSON format, including default configs with schemas, dimensions, and overrides. This endpoint is optimized for clients that prefer JSON format for configuration management.") @readonly @http(method: "POST", uri: "/config/json") @@ -186,6 +191,7 @@ enum MergeStrategy { REPLACE } +@mcpTool @documentation("Resolves and merges config values based on context conditions, applying overrides and merge strategies to produce the final configuration.") @http(method: "POST", uri: "/config/resolve") @tags(["Configuration Management"]) @@ -258,6 +264,7 @@ resource ConfigVersion { operations: [] } +@mcpTool @documentation("Retrieves a specific config version along with its metadata for audit and rollback purposes.") @readonly @http(method: "GET", uri: "/version/{id}") @@ -311,6 +318,7 @@ list ListVersionsOut { member: ListVersionsMember } +@mcpTool @documentation("Retrieves a paginated list of config versions with their metadata, hash values, and creation timestamps for audit and rollback purposes.") @readonly @http(method: "GET", uri: "/config/versions") @@ -334,6 +342,7 @@ operation ListVersions { } } +@mcpTool @documentation("Resolves and merges config values based on context conditions and identifier, applying overrides and merge strategies to produce the final configuration.") @http(method: "POST", uri: "/resolve") @tags(["Configuration Management"]) diff --git a/smithy/models/context.smithy b/smithy/models/context.smithy index 9c4cc39e0..75548d289 100644 --- a/smithy/models/context.smithy +++ b/smithy/models/context.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + list OverrideWithKeys { member: String } @@ -75,6 +77,7 @@ structure ContextResponse for Context { $last_modified_by } +@mcpTool @documentation("Creates a new context with specified conditions and overrides. Contexts define conditional rules for config management.") @idempotent @http(method: "PUT", uri: "/context") @@ -94,6 +97,7 @@ operation CreateContext with [GetOperation, WebhookOperation] { output: ContextResponse } +@mcpTool @documentation("Validates if a given context condition is well-formed") @idempotent @http(method: "PUT", uri: "/context/validate") @@ -106,6 +110,7 @@ operation ValidateContext { } } +@mcpTool @documentation("Retrieves detailed information about a specific context by its unique identifier, including conditions, overrides, and metadata.") @readonly @http(method: "GET", uri: "/context/{id}") @@ -120,6 +125,7 @@ operation GetContext with [GetOperation] { output: ContextResponse } +@mcpTool @documentation("Updates the condition of the mentioned context, if a context with the new condition already exists, it merges the override and effectively deleting the old context") @http(method: "PUT", uri: "/context/move/{id}") @tags(["Context Management"]) @@ -156,6 +162,7 @@ structure UpdateContextOverrideRequest for Context { $change_reason } +@mcpTool @documentation("Updates the overrides for an existing context. Allows modification of override values while maintaining the context's conditions.") @http(method: "PATCH", uri: "/context/overrides") @tags(["Context Management"]) @@ -175,6 +182,7 @@ operation UpdateOverride with [GetOperation, WebhookOperation] { output: ContextResponse } +@mcpTool @documentation("Retrieves context information by matching against provided conditions. Used to find contexts that would apply to specific scenarios.") @http(method: "POST", uri: "/context/get") @tags(["Context Management"]) @@ -198,6 +206,7 @@ list ListContextOut { member: ContextResponse } +@mcpTool @documentation("Retrieves a paginated list of contexts with support for filtering by creation date, modification date, weight, and other criteria.") @readonly @http(method: "GET", uri: "/context") @@ -239,6 +248,7 @@ operation ListContexts { } } +@mcpTool @documentation("Permanently removes a context from the workspace. This operation cannot be undone and will affect config resolution.") @idempotent @http(method: "DELETE", uri: "/context/{id}", code: 204) @@ -273,6 +283,7 @@ list WeightRecomputeResponses { member: WeightRecomputeResponse } +@mcpTool @documentation("Recalculates and updates the priority weights for all contexts in the workspace based on their dimensions.") @http(method: "PUT", uri: "/context/weight/recompute") @tags(["Context Management"]) @@ -342,6 +353,7 @@ list BulkOperationOutList { member: ContextActionOut } +@mcpTool @documentation("Executes multiple context operations (PUT, REPLACE, DELETE, MOVE) in a single atomic transaction for efficient batch processing.") @http(method: "PUT", uri: "/context/bulk-operations") @tags(["Context Management"]) diff --git a/smithy/models/default-config.smithy b/smithy/models/default-config.smithy index 6243f347d..a22764065 100644 --- a/smithy/models/default-config.smithy +++ b/smithy/models/default-config.smithy @@ -3,6 +3,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource DefaultConfig { identifiers: { key: String @@ -71,6 +73,7 @@ list ListDefaultConfigOut { member: DefaultConfigResponse } +@mcpTool @documentation("Retrieves a specific default config entry by its key, including its value, schema, function mappings, and metadata.") @readonly @http(method: "GET", uri: "/default-config/{key}") @@ -86,6 +89,7 @@ operation GetDefaultConfig with [GetOperation] { } // Operations +@mcpTool @documentation("Creates a new default config entry with specified key, value, schema, and metadata. Default configs serve as fallback values when no specific context matches.") @http(method: "POST", uri: "/default-config") @tags(["Default Configuration"]) @@ -94,6 +98,7 @@ operation CreateDefaultConfig with [WebhookOperation] { output: DefaultConfigResponse } +@mcpTool @documentation("Retrieves a paginated list of all default config entries in the workspace, including their values, schemas, and metadata.") @readonly @http(method: "GET", uri: "/default-config") @@ -111,6 +116,7 @@ operation ListDefaultConfigs { } } +@mcpTool @documentation("Updates an existing default config entry. Allows modification of value, schema, function mappings, and description while preserving the key identifier.") @idempotent @http(method: "PATCH", uri: "/default-config/{key}") @@ -140,6 +146,7 @@ operation UpdateDefaultConfig with [GetOperation, WebhookOperation] { output: DefaultConfigResponse } +@mcpTool @documentation("Permanently removes a default config entry from the workspace. This operation cannot be performed if it affects config resolution for contexts that rely on this fallback value.") @idempotent @http(method: "DELETE", uri: "/default-config/{key}", code: 204) diff --git a/smithy/models/dimension.smithy b/smithy/models/dimension.smithy index f2b16eaec..377615818 100644 --- a/smithy/models/dimension.smithy +++ b/smithy/models/dimension.smithy @@ -3,6 +3,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + union DimensionType { REGULAR: Unit LOCAL_COHORT: String @@ -90,6 +92,7 @@ list DimensionList { member: DimensionResponse } +@mcpTool @documentation("Creates a new dimension with the specified json schema. Dimensions define categorical attributes used for context-based config management.") @http(method: "POST", uri: "/dimension") @tags(["Dimensions"]) @@ -120,6 +123,7 @@ operation CreateDimension with [WebhookOperation] { output: DimensionResponse } +@mcpTool @documentation("Retrieves a paginated list of all dimensions in the workspace. Dimensions are returned with their details and metadata.") @readonly @http(method: "GET", uri: "/dimension") @@ -133,6 +137,7 @@ operation ListDimensions { } } +@mcpTool @documentation("Retrieves detailed information about a specific dimension, including its schema, cohort dependency graph, and configuration metadata.") @readonly @http(method: "GET", uri: "/dimension/{dimension}") @@ -147,6 +152,7 @@ operation GetDimension with [GetOperation] { output: DimensionResponse } +@mcpTool @documentation("Updates an existing dimension's configuration. Allows modification of schema, position, function mappings, and other properties while maintaining dependency relationships.") @idempotent @http(method: "PATCH", uri: "/dimension/{dimension}") @@ -176,6 +182,7 @@ operation UpdateDimension with [GetOperation, WebhookOperation] { output: DimensionResponse } +@mcpTool @documentation("Permanently removes a dimension from the workspace. This operation will fail if the dimension has active dependencies or is referenced by existing configurations.") @idempotent @http(method: "DELETE", uri: "/dimension/{dimension}", code: 204) diff --git a/smithy/models/experiment_config.smithy b/smithy/models/experiment_config.smithy index 7479aef4f..3fdf088d9 100644 --- a/smithy/models/experiment_config.smithy +++ b/smithy/models/experiment_config.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + @documentation("Represents a configuration of experiments that can be managed together.") resource ExperimentConfig { identifiers: { @@ -18,6 +20,7 @@ resource ExperimentConfig { ] } +@mcpTool @documentation("Retrieves the experiment configuration for a given workspace and organization. The response includes details of all experiment groups and experiments that match the specified filters.") @http(method: "POST", uri: "/experiment-config") @tags(["Experiment Config"]) diff --git a/smithy/models/experiment_groups.smithy b/smithy/models/experiment_groups.smithy index 2055e222d..ee3fa7c71 100644 --- a/smithy/models/experiment_groups.smithy +++ b/smithy/models/experiment_groups.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + enum ExperimentGroupSortOn { @documentation("Sort by name.") NAME = "name" @@ -154,6 +156,7 @@ structure ModifyMembersToGroupRequest for ExperimentGroup with [WorkspaceMixin] $member_experiment_ids } +@mcpTool @documentation("Adds members to an existing experiment group.") @http(method: "PATCH", uri: "/experiment-groups/{id}/add-members") @tags(["Experiment Groups"]) @@ -162,6 +165,7 @@ operation AddMembersToGroup with [GetOperation] { output: ExperimentGroupResponse } +@mcpTool @documentation("Removes members from an existing experiment group.") @http(method: "PATCH", uri: "/experiment-groups/{id}/remove-members") @tags(["Experiment Groups"]) @@ -170,6 +174,7 @@ operation RemoveMembersFromGroup with [GetOperation] { output: ExperimentGroupResponse } +@mcpTool @documentation("Creates a new experiment group.") @http(method: "POST", uri: "/experiment-groups") @tags(["Experiment Groups"]) @@ -178,6 +183,7 @@ operation CreateExperimentGroup { output: ExperimentGroupResponse } +@mcpTool @documentation("Retrieves an existing experiment group by its ID.") @readonly @http(method: "GET", uri: "/experiment-groups/{id}") @@ -210,6 +216,7 @@ structure UpdateExperimentGroupRequest for ExperimentGroup with [WorkspaceMixin] $traffic_percentage } +@mcpTool @documentation("Updates an existing experiment group. Allows partial updates to specified fields.") @idempotent @http(method: "PATCH", uri: "/experiment-groups/{id}") @@ -219,6 +226,7 @@ operation UpdateExperimentGroup with [GetOperation] { output: ExperimentGroupResponse } +@mcpTool @documentation("Deletes an experiment group.") @idempotent @http(method: "DELETE", uri: "/experiment-groups/{id}") @@ -238,6 +246,7 @@ list ExperimentGroupList { member: ExperimentGroupResponse } +@mcpTool @documentation("Lists experiment groups, with support for filtering and pagination.") @http(method: "POST", uri: "/experiment-groups/list") @tags(["Experiment Groups"]) diff --git a/smithy/models/experiments.smithy b/smithy/models/experiments.smithy index f63a97fe2..67c3507e5 100644 --- a/smithy/models/experiments.smithy +++ b/smithy/models/experiments.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Experiments { identifiers: { workspace_id: String @@ -232,6 +234,7 @@ structure ApplicableVariantsInput for Experiments with [WorkspaceMixin] { } // Operations +@mcpTool @documentation("Creates a new experiment with variants, context and conditions. You can optionally specify metrics and experiment group for tracking and analysis.") @http(method: "POST", uri: "/experiments") @tags(["Experimentation"]) @@ -241,6 +244,7 @@ operation CreateExperiment with [WebhookOperation] { } // Operations +@mcpTool @documentation("Updates the overrides for specific variants within an experiment, allowing modification of experiment behavior Updates the overrides for specific variants within an experiment, allowing modification of experiment behavior while it is in the created state.") @http(method: "PATCH", uri: "/experiments/{id}/overrides") @tags(["Experimentation"]) @@ -249,6 +253,7 @@ operation UpdateOverridesExperiment with [GetOperation, WebhookOperation] { output: ExperimentResponse } +@mcpTool @documentation("Concludes an inprogress experiment by selecting a winning variant and transitioning the experiment to a concluded state.") @idempotent @http(method: "PATCH", uri: "/experiments/{id}/conclude") @@ -271,6 +276,7 @@ operation ConcludeExperiment with [GetOperation, WebhookOperation] { output: ExperimentResponse } +@mcpTool @documentation("Discards an experiment without selecting a winner, effectively canceling the experiment and removing its effects.") @idempotent @http(method: "PATCH", uri: "/experiments/{id}/discard") @@ -288,6 +294,7 @@ operation DiscardExperiment with [GetOperation, WebhookOperation] { output: ExperimentResponse } +@mcpTool @documentation("Adjusts the traffic percentage allocation for an in-progress experiment, allowing gradual rollout or rollback of experimental features.") @idempotent @http(method: "PATCH", uri: "/experiments/{id}/ramp") @@ -308,6 +315,7 @@ operation RampExperiment with [GetOperation, WebhookOperation] { output: ExperimentResponse } +@mcpTool @documentation("Retrieves detailed information about a specific experiment, including its config, variants, status, and metrics.") @readonly @http(method: "GET", uri: "/experiments/{id}") @@ -322,6 +330,7 @@ operation GetExperiment with [GetOperation] { output: ExperimentResponse } +@mcpTool @documentation("Retrieves a paginated list of experiments with support for filtering by status, date range, name, creator, and experiment group.") @http(method: "POST", uri: "/experiments/list") @tags(["Experimentation"]) @@ -393,6 +402,7 @@ operation ListExperiment { } } +@mcpTool @documentation("Determines which experiment variants are applicable to a given context, used for experiment evaluation and variant selection.") @http(method: "POST", uri: "/experiments/applicable-variants") @tags(["Experimentation"]) @@ -401,6 +411,7 @@ operation ApplicableVariants { output: ApplicableVariantsOutput } +@mcpTool @documentation("Temporarily pauses an inprogress experiment, suspending its effects while preserving the experiment config for later resumption.") @idempotent @http(method: "PATCH", uri: "/experiments/{id}/pause") @@ -418,6 +429,7 @@ operation PauseExperiment with [GetOperation, WebhookOperation] { output: ExperimentResponse } +@mcpTool @documentation("Resumes a previously paused experiment, restoring its in-progress state and re-enabling variant evaluation.") @idempotent @http(method: "PATCH", uri: "/experiments/{id}/resume") diff --git a/smithy/models/functions.smithy b/smithy/models/functions.smithy index fda40d7a5..011318500 100644 --- a/smithy/models/functions.smithy +++ b/smithy/models/functions.smithy @@ -3,6 +3,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Function { identifiers: { function_name: String @@ -198,6 +200,7 @@ list FunctionListResponse { } // Operations +@mcpTool @documentation("Creates a new custom function for value_validation, value_compute, context_validation or change_reason_validation with specified code, runtime version, and function type.") @http(method: "POST", uri: "/function") @tags(["Functions"]) @@ -206,6 +209,7 @@ operation CreateFunction { output: FunctionResponse } +@mcpTool @documentation("Retrieves detailed information about a specific function including its published and draft versions, code, and metadata.") @readonly @http(method: "GET", uri: "/function/{function_name}") @@ -220,6 +224,7 @@ operation GetFunction with [GetOperation] { output: FunctionResponse } +@mcpTool @documentation("Retrieves a paginated list of all functions in the workspace with their basic information and current status.") @readonly @http(method: "GET", uri: "/function") @@ -236,6 +241,7 @@ operation ListFunction { } } +@mcpTool @documentation("Updates the draft version of an existing function with new code, runtime version, or description while preserving the published version.") @idempotent @http(method: "PATCH", uri: "/function/{function_name}") @@ -245,6 +251,7 @@ operation UpdateFunction with [GetOperation] { output: FunctionResponse } +@mcpTool @documentation("Permanently removes a function from the workspace, deleting both draft and published versions along with all associated code. It fails if already in use") @idempotent @http(method: "DELETE", uri: "/function/{function_name}", code: 204) @@ -257,6 +264,7 @@ operation DeleteFunction with [GetOperation] { } } +@mcpTool @documentation("Executes a function in test mode with provided input parameters to validate its behavior before publishing or deployment.") @idempotent @http(method: "POST", uri: "/function/{function_name}/{stage}/test") @@ -281,6 +289,7 @@ operation Test with [GetOperation] { output: FunctionExecutionResponse } +@mcpTool @documentation("Publishes the draft version of a function, making it the active version used for value_validation, value_compute, context_validation or change_reason_validation in the system.") @idempotent @http(method: "PATCH", uri: "/function/{function_name}/publish") diff --git a/smithy/models/organisation.smithy b/smithy/models/organisation.smithy index 62e431bb1..2947711fb 100644 --- a/smithy/models/organisation.smithy +++ b/smithy/models/organisation.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Organisation { identifiers: { id: String @@ -95,6 +97,7 @@ list OrganisationList { } // Operations +@mcpTool @documentation("Creates a new organisation with specified name and administrator email. This is the top-level entity that contains workspaces and manages organizational-level settings.") @http(method: "POST", uri: "/superposition/organisations") @tags(["Organisation Management"]) @@ -103,6 +106,7 @@ operation CreateOrganisation { output: OrganisationResponse } +@mcpTool @documentation("Retrieves detailed information about a specific organisation including its status, contact details, and administrative metadata.") @readonly @http(method: "GET", uri: "/superposition/organisations/{id}") @@ -117,6 +121,7 @@ operation GetOrganisation with [GetOperation] { output: OrganisationResponse } +@mcpTool @documentation("Updates an existing organisation's information including contact details, status, and administrative properties.") @idempotent @http(method: "PATCH", uri: "/superposition/organisations/{id}") @@ -126,6 +131,7 @@ operation UpdateOrganisation with [GetOperation] { output: OrganisationResponse } +@mcpTool @documentation("Retrieves a paginated list of all organisations with their basic information, creation details, and current status.") @readonly @http(method: "GET", uri: "/superposition/organisations") diff --git a/smithy/models/secret.smithy b/smithy/models/secret.smithy index fdda45e8d..53dc7fbaa 100644 --- a/smithy/models/secret.smithy +++ b/smithy/models/secret.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + @documentation("Secrets are encrypted key-value pairs stored at the workspace level. Secret values are encrypted with workspace-specific encryption keys and support key rotation.") resource Secret { identifiers: { @@ -61,6 +63,7 @@ enum SecretSortOn { LAST_MODIFIED_AT = "last_modified_at" } +@mcpTool @documentation("Creates a new encrypted secret with the specified name and value. The secret is encrypted with the workspace's current encryption key. Secret values are never returned in responses for security.") @http(method: "POST", uri: "/secrets") @tags(["Secrets"]) @@ -83,6 +86,7 @@ operation CreateSecret { output: SecretResponse } +@mcpTool @documentation("Updates an existing secret's value or description. The value is re-encrypted with the current workspace encryption key. Returns masked value.") @idempotent @http(method: "PATCH", uri: "/secrets/{name}") @@ -105,6 +109,7 @@ operation UpdateSecret with [GetOperation] { output: SecretResponse } +@mcpTool @documentation("Retrieves a paginated list of all secrets in the workspace with optional filtering and sorting. All secret values are masked.") @readonly @http(method: "GET", uri: "/secrets") @@ -141,6 +146,7 @@ operation ListSecrets { } } +@mcpTool @documentation("Retrieves detailed information about a specific secret by its name. The value is masked for security.") @readonly @http(method: "GET", uri: "/secrets/{name}") @@ -155,6 +161,7 @@ operation GetSecret with [GetOperation] { output: SecretResponse } +@mcpTool @documentation("Permanently deletes a secret from the workspace. The encrypted value is removed and cannot be recovered.") @idempotent @http(method: "DELETE", uri: "/secrets/{name}") @@ -178,6 +185,7 @@ resource MasterKey { ] } +@mcpTool @documentation("Rotates the master encryption key across all workspaces") @idempotent @http(method: "POST", uri: "/master-encryption-key/rotate") diff --git a/smithy/models/type-templates.smithy b/smithy/models/type-templates.smithy index 024d31584..e330de1db 100644 --- a/smithy/models/type-templates.smithy +++ b/smithy/models/type-templates.smithy @@ -3,6 +3,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource TypeTemplates { identifiers: { type_name: String @@ -85,6 +87,7 @@ list TypeTemplatesList { member: TypeTemplatesResponse } +@mcpTool @documentation("Retrieves detailed information about a specific type template including its schema and metadata.") @readonly @http(method: "GET", uri: "/types/{type_name}") @@ -100,6 +103,7 @@ operation GetTypeTemplate with [GetOperation] { } // Operations +@mcpTool @documentation("Creates a new type template with specified schema definition, providing reusable type definitions for config validation.") @http(method: "POST", uri: "/types") @tags(["Type Templates"]) @@ -108,6 +112,7 @@ operation CreateTypeTemplates { output: TypeTemplatesResponse } +@mcpTool @documentation("Retrieves a paginated list of all type templates in the workspace, including their schemas and metadata for type management.") @readonly @http(method: "GET", uri: "/types") @@ -121,6 +126,7 @@ operation GetTypeTemplatesList { } } +@mcpTool @documentation("Updates an existing type template's schema definition and metadata while preserving its identifier and usage history.") @idempotent @http(method: "PATCH", uri: "/types/{type_name}") @@ -130,6 +136,7 @@ operation UpdateTypeTemplates with [GetOperation] { output: TypeTemplatesResponse } +@mcpTool @documentation("Permanently removes a type template from the workspace. No checks performed while deleting") @idempotent @http(method: "DELETE", uri: "/types/{type_name}") diff --git a/smithy/models/variable.smithy b/smithy/models/variable.smithy index 18eb1387a..890fc4b29 100644 --- a/smithy/models/variable.smithy +++ b/smithy/models/variable.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + @documentation("Variables are key-value pairs used to store configuration values that can be referenced in contexts, webhooks, and other parts of the system.") resource Variable { identifiers: { @@ -63,6 +65,7 @@ enum VariableSortOn { LAST_MODIFIED_AT = "last_modified_at" } +@mcpTool @documentation("Creates a new variable with the specified name and value.") @http(method: "POST", uri: "/variables") @tags(["Variables"]) @@ -84,6 +87,7 @@ operation CreateVariable { output: VariableResponse } +@mcpTool @documentation("Updates an existing variable's value, description, or tags.") @idempotent @http(method: "PATCH", uri: "/variables/{name}") @@ -105,6 +109,7 @@ operation UpdateVariable with [GetOperation] { output: VariableResponse } +@mcpTool @documentation("Retrieves a paginated list of all variables in the workspace with optional filtering and sorting.") @readonly @http(method: "GET", uri: "/variables") @@ -141,6 +146,7 @@ operation ListVariables { } } +@mcpTool @documentation("Retrieves detailed information about a specific variable by its name.") @readonly @http(method: "GET", uri: "/variables/{name}") @@ -155,6 +161,7 @@ operation GetVariable with [GetOperation] { output: VariableResponse } +@mcpTool @documentation("Permanently deletes a variable from the workspace.") @idempotent @http(method: "DELETE", uri: "/variables/{name}") diff --git a/smithy/models/webhook.smithy b/smithy/models/webhook.smithy index 26975382b..aeb07fcef 100644 --- a/smithy/models/webhook.smithy +++ b/smithy/models/webhook.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Webhook { identifiers: { workspace_id: String @@ -103,6 +105,7 @@ list WebhookList { } // Operations +@mcpTool @documentation("Creates a new webhook config to receive HTTP notifications when specified events occur in the system.") @http(method: "POST", uri: "/webhook") @tags(["Webhooks"]) @@ -137,6 +140,7 @@ operation CreateWebhook { output: WebhookResponse } +@mcpTool @documentation("Updates an existing webhook config, allowing modification of URL, events, headers, and other webhook properties.") @idempotent @http(method: "PATCH", uri: "/webhook/{name}") @@ -168,6 +172,7 @@ operation UpdateWebhook with [GetOperation] { output: WebhookResponse } +@mcpTool @documentation("Retrieves a paginated list of all webhook configs in the workspace, including their status and config details.") @readonly @http(method: "GET", uri: "/webhook") @@ -181,6 +186,7 @@ operation ListWebhook { } } +@mcpTool @documentation("Retrieves detailed information about a specific webhook config, including its events, headers, and trigger history.") @readonly @http(method: "GET", uri: "/webhook/{name}") @@ -195,6 +201,7 @@ operation GetWebhook with [GetOperation] { output: WebhookResponse } +@mcpTool @documentation("Retrieves a webhook configuration based on a specific event type, allowing users to find which webhook is set to trigger for that event.") @http(method: "GET", uri: "/webhook/event/{event}") @tags(["Webhooks"]) @@ -209,6 +216,7 @@ operation GetWebhookByEvent with [GetOperation] { output: WebhookResponse } +@mcpTool @documentation("Permanently removes a webhook config from the workspace, stopping all future event notifications to that endpoint.") @idempotent @http(method: "DELETE", uri: "/webhook/{name}", code: 204) diff --git a/smithy/models/workspace.smithy b/smithy/models/workspace.smithy index 9831c033d..11bb31d98 100644 --- a/smithy/models/workspace.smithy +++ b/smithy/models/workspace.smithy @@ -2,6 +2,8 @@ $version: "2.0" namespace io.superposition +use software.amazon.smithy.mcp#mcpTool + resource Workspace { identifiers: { workspace_name: String @@ -150,6 +152,7 @@ list WorkspaceList { member: WorkspaceResponse } +@mcpTool @documentation("Retrieves detailed information about a specific workspace including its configuration and metadata.") @readonly @http(method: "GET", uri: "/workspaces/{workspace_name}") @@ -165,6 +168,7 @@ operation GetWorkspace with [GetOperation] { } // Operations +@mcpTool @documentation("Creates a new workspace within an organisation, including database schema setup and isolated environment for config management with specified admin and settings.") @http(method: "POST", uri: "/workspaces") @tags(["Workspace Management"]) @@ -173,6 +177,7 @@ operation CreateWorkspace { output: WorkspaceResponse } +@mcpTool @documentation("Updates an existing workspace configuration, allowing modification of admin settings, mandatory dimensions, and workspace properties. Validates config version existence if provided.") @idempotent @http(method: "PATCH", uri: "/workspaces/{workspace_name}") @@ -182,6 +187,7 @@ operation UpdateWorkspace with [GetOperation] { output: WorkspaceResponse } +@mcpTool @documentation("Retrieves a paginated list of all workspaces with optional filtering by workspace name, including their status, config details, and administrative information.") @readonly @http(method: "GET", uri: "/workspaces") @@ -195,6 +201,7 @@ operation ListWorkspace { } } +@mcpTool @documentation("Migrates the workspace database schema to the new version of the template") @idempotent @http(method: "POST", uri: "/workspaces/{workspace_name}/db/migrate") @@ -204,6 +211,7 @@ operation MigrateWorkspaceSchema with [GetOperation] { output: WorkspaceResponse } +@mcpTool @documentation("Rotates the workspace encryption key. Generates a new encryption key and re-encrypts all secrets with the new key. This is a critical operation that should be done during low-traffic periods.") @http(method: "POST", uri: "/workspaces/{workspace_name}/rotate-encryption-key") @idempotent diff --git a/smithy/patches/mcp-rust.patch b/smithy/patches/mcp-rust.patch new file mode 100644 index 000000000..6e83cc597 --- /dev/null +++ b/smithy/patches/mcp-rust.patch @@ -0,0 +1,19 @@ +diff --git a/crates/superposition_mcp/Cargo.toml b/crates/superposition_mcp/Cargo.toml +--- a/crates/superposition_mcp/Cargo.toml ++++ b/crates/superposition_mcp/Cargo.toml +@@ -1,10 +1,13 @@ + [package] + name = "superposition_mcp" +-version = "0.1.0" ++version.workspace = true + edition = "2021" ++license = { workspace = true } ++homepage = { workspace = true } ++readme = "README.md" + + [dependencies] +-smithy-mcp-runtime = "0.1.0" ++smithy-mcp-runtime = { git = "https://github.com/juspay/smithy-mcp-generator.git", rev = "6ec7445755c1410c052947f00eef1dd65011aadc", features = ["stdio", "http"] } + superposition_sdk = { path = "../superposition_sdk" } + serde = { version = "1", features = ["derive"] } + serde_json = "1" diff --git a/smithy/smithy-build.json b/smithy/smithy-build.json index 0fed00e2a..6c4e1b5fa 100644 --- a/smithy/smithy-build.json +++ b/smithy/smithy-build.json @@ -18,7 +18,9 @@ "software.amazon.smithy.java:client-core:0.0.1", "software.amazon.smithy.java.codegen:plugins:0.0.1", "in.juspay.smithy.haskell:client-codegen:0.0.8", - "software.amazon.smithy.java:aws-client-restjson:0.0.1" + "software.amazon.smithy.java:aws-client-restjson:0.0.1", + "in.juspay.smithy:smithy-mcp-codegen:0.1.0", + "in.juspay.smithy:smithy-mcp-traits:0.1.0" ] }, "plugins": { @@ -90,6 +92,13 @@ "description": "API documentation for Superposition" } } + }, + "mcp-rust": { + "service": "io.superposition#Superposition", + "package": "superposition_mcp", + "clientSdk": "smithy-rs", + "clientCrate": "superposition_sdk", + "runtimeVersion": "0.1.0" } } }