Compare commits

...

2 Commits

Author SHA1 Message Date
Casey Chow
3966edd558 fix: localize reqwest 0.13 to rmcp-client 2026-03-07 11:40:05 -05:00
Casey Chow
7f96f3efd7 build: upgrade rmcp to 0.17.0 2026-03-07 11:40:04 -05:00
6 changed files with 746 additions and 50 deletions

26
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

360
codex-rs/Cargo.lock generated
View File

@@ -193,7 +193,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -828,7 +828,6 @@ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.4.0",
"http-body",
@@ -844,13 +843,11 @@ dependencies = [
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -869,7 +866,6 @@ dependencies = [
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -1225,7 +1221,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[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.0",
]
[[package]]
@@ -1235,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"chacha20 0.9.1",
"cipher",
"poly1305",
"zeroize",
@@ -1393,7 +1400,7 @@ dependencies = [
"http 1.4.0",
"pretty_assertions",
"regex-lite",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.18",
@@ -1542,7 +1549,7 @@ version = "0.0.0"
dependencies = [
"codex-package-manager",
"pretty_assertions",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
@@ -1574,7 +1581,7 @@ dependencies = [
"codex-core",
"codex-protocol",
"pretty_assertions",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
]
@@ -1664,7 +1671,7 @@ dependencies = [
"opentelemetry",
"opentelemetry_sdk",
"rand 0.9.2",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.18",
@@ -1716,7 +1723,7 @@ dependencies = [
"owo-colors",
"pretty_assertions",
"ratatui",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"supports-color 3.0.2",
@@ -1843,7 +1850,7 @@ dependencies = [
"pretty_assertions",
"rand 0.9.2",
"regex-lite",
"reqwest",
"reqwest 0.12.28",
"rmcp",
"schemars 0.8.22",
"seccompiler",
@@ -2073,7 +2080,7 @@ name = "codex-lmstudio"
version = "0.0.0"
dependencies = [
"codex-core",
"reqwest",
"reqwest 0.12.28",
"serde_json",
"tokio",
"tracing",
@@ -2093,7 +2100,7 @@ dependencies = [
"core_test_support",
"pretty_assertions",
"rand 0.9.2",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
@@ -2175,7 +2182,7 @@ dependencies = [
"codex-core",
"futures",
"pretty_assertions",
"reqwest",
"reqwest 0.12.28",
"semver",
"serde_json",
"tokio",
@@ -2202,7 +2209,7 @@ dependencies = [
"opentelemetry_sdk",
"os_info",
"pretty_assertions",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"strum_macros 0.28.0",
@@ -2221,7 +2228,7 @@ dependencies = [
"fd-lock",
"flate2",
"pretty_assertions",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
@@ -2278,7 +2285,7 @@ dependencies = [
"codex-process-hardening",
"ctor 0.6.3",
"libc",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"tiny_http",
@@ -2300,7 +2307,7 @@ dependencies = [
"keyring",
"oauth2",
"pretty_assertions",
"reqwest",
"reqwest 0.13.2",
"rmcp",
"schemars 0.8.22",
"serde",
@@ -2487,7 +2494,7 @@ dependencies = [
"ratatui",
"ratatui-macros",
"regex-lite",
"reqwest",
"reqwest 0.12.28",
"rmcp",
"serde",
"serde_json",
@@ -2798,7 +2805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"proptest",
"serde_core",
]
@@ -2904,7 +2911,7 @@ dependencies = [
"notify",
"pretty_assertions",
"regex-lite",
"reqwest",
"reqwest 0.12.28",
"serde_json",
"shlex",
"tempfile",
@@ -2967,6 +2974,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 = "crc"
version = "3.4.0"
@@ -3135,7 +3151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"fiat-crypto",
"rustc_version",
@@ -4279,11 +4295,25 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
"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.0",
"wasip2",
"wasip3",
]
[[package]]
name = "gif"
version = "0.14.1"
@@ -4902,6 +4932,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"
@@ -5371,6 +5407,12 @@ dependencies = [
"spin",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.182"
@@ -6099,7 +6141,7 @@ dependencies = [
"getrandom 0.2.17",
"http 1.4.0",
"rand 0.8.5",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"serde_path_to_error",
@@ -6460,7 +6502,7 @@ dependencies = [
"bytes",
"http 1.4.0",
"opentelemetry",
"reqwest",
"reqwest 0.12.28",
]
[[package]]
@@ -6475,7 +6517,7 @@ dependencies = [
"opentelemetry-proto",
"opentelemetry_sdk",
"prost",
"reqwest",
"reqwest 0.12.28",
"serde_json",
"thiserror 2.0.18",
"tokio",
@@ -6821,7 +6863,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -6934,6 +6976,16 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.114",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -7110,6 +7162,7 @@ version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.3.4",
"lru-slab",
@@ -7154,6 +7207,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 = "radix_trie"
version = "0.2.1"
@@ -7505,6 +7564,17 @@ dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.2",
"rand_core 0.10.0",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -7543,6 +7613,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "rand_xorshift"
version = "0.4.0"
@@ -7761,11 +7837,51 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"wasm-streams 0.4.2",
"web-sys",
"webpki-roots 1.0.5",
]
[[package]]
name = "reqwest"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http 1.4.0",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams 0.5.0",
"web-sys",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
@@ -7788,12 +7904,11 @@ dependencies = [
[[package]]
name = "rmcp"
version = "0.15.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee"
checksum = "8a0ce46f9101dc911f07e1468084c057839d15b08040d110820c5513312ef56a"
dependencies = [
"async-trait",
"axum",
"base64 0.22.1",
"bytes",
"chrono",
@@ -7805,8 +7920,8 @@ dependencies = [
"pastey",
"pin-project-lite",
"process-wrap",
"rand 0.9.2",
"reqwest",
"rand 0.10.0",
"reqwest 0.13.2",
"rmcp-macros",
"schemars 1.2.1",
"serde",
@@ -7824,9 +7939,9 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "0.15.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe"
checksum = "abad6f5f46e220e3bda2fc90fd1ad64c1c2a2bd716d52c845eb5c9c64cda7542"
dependencies = [
"darling 0.23.0",
"proc-macro2",
@@ -8004,6 +8119,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.9"
@@ -8321,7 +8463,7 @@ checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"reqwest 0.12.28",
"sentry-actix",
"sentry-backtrace",
"sentry-contexts",
@@ -8633,7 +8775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -8650,7 +8792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -10463,6 +10605,15 @@ dependencies = [
"wit-bindgen",
]
[[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",
]
[[package]]
name = "wasite"
version = "0.1.0"
@@ -10528,6 +10679,28 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap 2.13.0",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -10541,6 +10714,31 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.10.0",
"hashbrown 0.15.5",
"indexmap 2.13.0",
"semver",
]
[[package]]
name = "wayland-backend"
version = "0.3.12"
@@ -11341,6 +11539,88 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap 2.13.0",
"prettyplease",
"syn 2.0.114",
"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.114",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"indexmap 2.13.0",
"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.13.0",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "wl-clipboard-rs"

View File

@@ -230,7 +230,7 @@ ratatui-macros = "0.6.0"
regex = "1.12.3"
regex-lite = "0.1.8"
reqwest = "0.12"
rmcp = { version = "0.15.0", default-features = false }
rmcp = { version = "0.17.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
rustls = { version = "0.23", default-features = false, features = [
"ring",

View File

@@ -11,6 +11,7 @@ workspace = true
anyhow = "1"
axum = { workspace = true, default-features = false, features = [
"http1",
"json",
"tokio",
] }
codex-keyring-store = { workspace = true }
@@ -20,10 +21,10 @@ codex-utils-home-dir = { workspace = true }
futures = { workspace = true, default-features = false, features = ["std"] }
keyring = { workspace = true, features = ["crypto-rust"] }
oauth2 = "5"
reqwest = { version = "0.12", default-features = false, features = [
reqwest = { version = "0.13.2", default-features = false, features = [
"json",
"stream",
"rustls-tls",
"rustls",
] }
rmcp = { workspace = true, default-features = false, features = [
"auth",

View File

@@ -18,6 +18,8 @@ use reqwest::header::ACCEPT;
use reqwest::header::AUTHORIZATION;
use reqwest::header::CONTENT_TYPE;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue;
use reqwest::header::WWW_AUTHENTICATE;
use rmcp::model::CallToolRequestParams;
use rmcp::model::CallToolResult;
@@ -97,6 +99,16 @@ impl StreamableHttpResponseClient {
) -> StreamableHttpError<StreamableHttpResponseClientError> {
StreamableHttpError::Client(StreamableHttpResponseClientError::from(error))
}
fn apply_custom_headers(
mut request: reqwest::RequestBuilder,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> reqwest::RequestBuilder {
for (name, value) in custom_headers {
request = request.header(name, value);
}
request
}
}
#[derive(Debug, thiserror::Error)]
@@ -116,6 +128,7 @@ impl StreamableHttpClient for StreamableHttpResponseClient {
message: rmcp::model::ClientJsonRpcMessage,
session_id: Option<Arc<str>>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> std::result::Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>> {
let mut request = self
.inner
@@ -127,6 +140,7 @@ impl StreamableHttpClient for StreamableHttpResponseClient {
if let Some(session_id_value) = session_id.as_ref() {
request = request.header(HEADER_SESSION_ID, session_id_value.as_ref());
}
request = Self::apply_custom_headers(request, custom_headers);
let response = request
.json(&message)
@@ -217,16 +231,19 @@ impl StreamableHttpClient for StreamableHttpResponseClient {
uri: Arc<str>,
session: Arc<str>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> std::result::Result<(), StreamableHttpError<Self::Error>> {
let mut request_builder = self.inner.delete(uri.as_ref());
if let Some(auth_header) = auth_token {
request_builder = request_builder.bearer_auth(auth_header);
}
let response = request_builder
.header(HEADER_SESSION_ID, session.as_ref())
.send()
.await
.map_err(StreamableHttpResponseClient::reqwest_error)?;
let response = Self::apply_custom_headers(
request_builder.header(HEADER_SESSION_ID, session.as_ref()),
custom_headers,
)
.send()
.await
.map_err(StreamableHttpResponseClient::reqwest_error)?;
if response.status() == reqwest::StatusCode::METHOD_NOT_ALLOWED {
return Ok(());
@@ -244,6 +261,7 @@ impl StreamableHttpClient for StreamableHttpResponseClient {
session_id: Arc<str>,
last_event_id: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> std::result::Result<
BoxStream<'static, std::result::Result<Sse, sse_stream::Error>>,
StreamableHttpError<Self::Error>,
@@ -259,6 +277,7 @@ impl StreamableHttpClient for StreamableHttpResponseClient {
if let Some(auth_header) = auth_token {
request_builder = request_builder.bearer_auth(auth_header);
}
request_builder = Self::apply_custom_headers(request_builder, custom_headers);
let response = request_builder
.send()

View File

@@ -0,0 +1,374 @@
use std::convert::Infallible;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use axum::Router;
use axum::body::Body;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::http::header::CONTENT_TYPE;
use axum::response::IntoResponse;
use axum::response::Response;
use axum::routing::post;
use codex_rmcp_client::ElicitationAction;
use codex_rmcp_client::ElicitationResponse;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_rmcp_client::RmcpClient;
use futures::FutureExt as _;
use futures::StreamExt as _;
use futures::stream;
use pretty_assertions::assert_eq;
use rmcp::model::ClientCapabilities;
use rmcp::model::CreateElicitationRequestParams;
use rmcp::model::ElicitationCapability;
use rmcp::model::FormElicitationCapability;
use rmcp::model::Implementation;
use rmcp::model::InitializeRequestParams;
use rmcp::model::NumberOrString;
use rmcp::model::ProtocolVersion;
use serde_json::Value;
use serde_json::json;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio::time::sleep;
const SESSION_ID: &str = "session-1";
const ELICITATION_ID: &str = "elicitation-1";
const SSE_EVENT_ID: &str = "approval-event-1";
#[derive(Debug, Clone, PartialEq, Eq)]
struct GetRequestRecord {
session_id: Option<String>,
last_event_id: Option<String>,
}
#[derive(Debug, Clone)]
struct PostRequestRecord {
session_id: Option<String>,
body: Value,
}
#[derive(Default)]
struct ApprovalFlowState {
get_requests: Mutex<Vec<GetRequestRecord>>,
post_requests: Mutex<Vec<PostRequestRecord>>,
observed_elicitation: Mutex<Option<(NumberOrString, CreateElicitationRequestParams)>>,
}
fn init_params() -> InitializeRequestParams {
InitializeRequestParams {
meta: None,
capabilities: ClientCapabilities {
experimental: None,
extensions: None,
roots: None,
sampling: None,
elicitation: Some(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: None,
}),
url: None,
}),
tasks: None,
},
client_info: Implementation {
name: "codex-test".into(),
version: "0.0.0-test".into(),
title: Some("Codex rmcp elicitation pause test".into()),
description: None,
icons: None,
website_url: None,
},
protocol_version: ProtocolVersion::V_2025_06_18,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn streamable_http_approval_elicitation_pause_flow_records_current_behavior() -> Result<()> {
let state = Arc::new(ApprovalFlowState::default());
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let router = Router::new()
.route("/mcp", post(mcp_post).get(mcp_get).delete(mcp_delete))
.with_state(Arc::clone(&state));
let server = tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
let client = Arc::new(
RmcpClient::new_streamable_http_client(
"approval-test",
&format!("http://{addr}/mcp"),
Some("test-bearer".to_string()),
None,
None,
OAuthCredentialsStoreMode::File,
)
.await?,
);
let state_for_elicitation = Arc::clone(&state);
client
.initialize(
init_params(),
Some(Duration::from_secs(2)),
Box::new(move |request_id, request| {
let state_for_elicitation = Arc::clone(&state_for_elicitation);
async move {
{
let mut observed = state_for_elicitation.observed_elicitation.lock().await;
*observed = Some((request_id, request));
}
Ok(ElicitationResponse {
action: ElicitationAction::Accept,
content: Some(json!({
"approved": true,
})),
meta: None,
})
}
.boxed()
}),
)
.await?;
let client_for_call = Arc::clone(&client);
let call_task = tokio::spawn(async move {
client_for_call
.call_tool(
"approval_required".to_string(),
Some(json!({})),
Some(Duration::from_millis(250)),
)
.await
});
wait_until(Duration::from_secs(1), || {
let state = Arc::clone(&state);
async move {
state.observed_elicitation.lock().await.is_some()
&& state
.post_requests
.lock()
.await
.iter()
.any(|record| is_elicitation_response(&record.body))
}
})
.await?;
sleep(Duration::from_millis(100)).await;
let call_error = call_task
.await?
.expect_err("tools/call should still be awaiting a result");
assert!(
call_error
.to_string()
.contains("timed out awaiting tools/call"),
"expected tools/call timeout after accepted elicitation, got: {call_error:#}",
);
let observed_elicitation = state.observed_elicitation.lock().await.clone();
assert_eq!(
observed_elicitation,
Some((
NumberOrString::String(ELICITATION_ID.to_string().into()),
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Approve this tool call?".to_string(),
requested_schema: serde_json::from_value(json!({
"type": "object",
"properties": {
"approved": { "type": "boolean" }
},
"required": ["approved"],
"additionalProperties": false
}))?,
}
))
);
let post_requests = state.post_requests.lock().await.clone();
let elicitation_response = post_requests
.iter()
.find(|record| is_elicitation_response(&record.body))
.expect("elicitation response POST");
assert_eq!(elicitation_response.session_id.as_deref(), Some(SESSION_ID));
assert_eq!(
elicitation_response.body,
json!({
"jsonrpc": "2.0",
"id": ELICITATION_ID,
"result": {
"action": "accept",
"content": {
"approved": true
}
}
})
);
let get_requests = state.get_requests.lock().await.clone();
assert_eq!(
get_requests.first(),
Some(&GetRequestRecord {
session_id: Some(SESSION_ID.to_string()),
last_event_id: None,
})
);
assert!(
get_requests
.iter()
.any(|record| { record.last_event_id.as_deref() == Some(SSE_EVENT_ID) }),
"expected clean close to trigger a reconnect with Last-Event-ID",
);
server.abort();
let _ = server.await;
Ok(())
}
async fn mcp_post(
State(state): State<Arc<ApprovalFlowState>>,
headers: HeaderMap,
body: String,
) -> Response {
let json: Value = match serde_json::from_str(&body) {
Ok(json) => json,
Err(error) => panic!("invalid JSON-RPC request: {error}: {body}"),
};
let session_id = headers
.get("mcp-session-id")
.and_then(|value| value.to_str().ok())
.map(str::to_string);
state.post_requests.lock().await.push(PostRequestRecord {
session_id,
body: json.clone(),
});
let method = json.get("method").and_then(Value::as_str);
match method {
Some("initialize") => json_response(
json!({
"jsonrpc": "2.0",
"id": match json.get("id").cloned() {
Some(id) => id,
None => panic!("initialize request missing id: {json}"),
},
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {},
"elicitation": {}
},
"serverInfo": {
"name": "approval-test-server",
"version": "0.0.0-test"
}
}
}),
Some(SESSION_ID),
),
Some("notifications/initialized") => StatusCode::ACCEPTED.into_response(),
Some("tools/call") => match Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "text/event-stream")
.header("mcp-session-id", SESSION_ID)
.body(Body::from(format!(
"id: {SSE_EVENT_ID}\nretry: 25\ndata: {}\n\n",
json!({
"jsonrpc": "2.0",
"id": ELICITATION_ID,
"method": "elicitation/create",
"params": {
"message": "Approve this tool call?",
"requestedSchema": {
"type": "object",
"properties": {
"approved": { "type": "boolean" }
},
"required": ["approved"],
"additionalProperties": false
}
}
})
))) {
Ok(response) => response,
Err(error) => panic!("failed to build SSE response: {error}"),
},
None if is_elicitation_response(&json) => StatusCode::ACCEPTED.into_response(),
other => panic!("unexpected POST method: {other:?} body={json}"),
}
}
async fn mcp_get(State(state): State<Arc<ApprovalFlowState>>, headers: HeaderMap) -> Response {
state.get_requests.lock().await.push(GetRequestRecord {
session_id: headers
.get("mcp-session-id")
.and_then(|value| value.to_str().ok())
.map(str::to_string),
last_event_id: headers
.get("last-event-id")
.and_then(|value| value.to_str().ok())
.map(str::to_string),
});
let body_stream =
stream::once(async { Ok::<Bytes, Infallible>(Bytes::from_static(b": keep-alive\n\n")) })
.chain(stream::pending::<Result<Bytes, Infallible>>());
match Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "text/event-stream")
.body(Body::from_stream(body_stream))
{
Ok(response) => response,
Err(error) => panic!("failed to build GET SSE response: {error}"),
}
}
async fn mcp_delete() -> Response {
StatusCode::NO_CONTENT.into_response()
}
fn json_response(body: Value, session_id: Option<&str>) -> Response {
let mut response = (StatusCode::OK, axum::Json(body)).into_response();
if let Some(session_id) = session_id {
let session_id = match session_id.parse() {
Ok(session_id) => session_id,
Err(error) => panic!("invalid session id header {session_id}: {error}"),
};
response.headers_mut().insert("mcp-session-id", session_id);
}
response
}
fn is_elicitation_response(body: &Value) -> bool {
body.get("method").is_none()
&& body.get("id") == Some(&Value::String(ELICITATION_ID.to_string()))
&& body.get("result").is_some()
}
async fn wait_until<F, Fut>(timeout: Duration, predicate: F) -> Result<()>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = bool>,
{
tokio::time::timeout(timeout, async {
loop {
if predicate().await {
break;
}
sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}