mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Update rmcp to 1.7.0 (#24763)
WIll make it easier to uprev when the new draft spec is supported. Also updates reqwest where needed for compatibility but doesn't update it everywhere since this is already a large diff. The new version of rmcp handles certain kinds of authentication failures differently, this patch includes support for identifying the failing scope in a WWW-Authenticate header.
This commit is contained in:
committed by
GitHub
Unverified
parent
c57dee98b7
commit
910578792f
Generated
+11
-2
File diff suppressed because one or more lines are too long
Generated
+178
-43
@@ -1147,7 +1147,6 @@ dependencies = [
|
||||
"axum-core",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
@@ -1163,7 +1162,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1 0.10.6",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
@@ -1171,7 +1169,6 @@ dependencies = [
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1190,7 +1187,6 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1830,7 +1826,7 @@ dependencies = [
|
||||
"jsonwebtoken",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -1884,7 +1880,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1963,7 +1959,7 @@ dependencies = [
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2026,7 +2022,7 @@ dependencies = [
|
||||
"futures",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -2195,7 +2191,7 @@ dependencies = [
|
||||
"codex-model-provider",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -2331,7 +2327,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
@@ -2393,7 +2389,7 @@ dependencies = [
|
||||
"owo-colors",
|
||||
"pretty_assertions",
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"supports-color 3.0.2",
|
||||
@@ -2603,7 +2599,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2676,7 +2672,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2805,7 +2801,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"prost 0.14.3",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -3091,7 +3087,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-model-provider-info",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3125,7 +3121,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -3389,7 +3385,7 @@ dependencies = [
|
||||
"codex-model-provider-info",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -3417,7 +3413,7 @@ dependencies = [
|
||||
"opentelemetry_sdk",
|
||||
"os_info",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum_macros 0.28.0",
|
||||
@@ -3469,7 +3465,7 @@ dependencies = [
|
||||
"landlock",
|
||||
"pretty_assertions",
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"schemars 0.8.22",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
@@ -3517,7 +3513,7 @@ dependencies = [
|
||||
"ctor 0.6.3",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tiny_http",
|
||||
@@ -3545,7 +3541,7 @@ dependencies = [
|
||||
"keyring",
|
||||
"oauth2",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"reqwest 0.13.4",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3879,7 +3875,7 @@ dependencies = [
|
||||
"ratatui",
|
||||
"ratatui-macros",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4450,7 +4446,7 @@ dependencies = [
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"similar",
|
||||
@@ -4495,7 +4491,7 @@ dependencies = [
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
@@ -8239,19 +8235,68 @@ dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
@@ -8460,7 +8505,7 @@ source = "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"glib",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"livekit-protocol",
|
||||
@@ -8901,7 +8946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
@@ -8920,7 +8965,7 @@ version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9203,7 +9248,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",
|
||||
@@ -9398,7 +9443,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
@@ -9564,7 +9609,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"http 1.4.0",
|
||||
"opentelemetry",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9579,7 +9624,7 @@ dependencies = [
|
||||
"opentelemetry-proto",
|
||||
"opentelemetry_sdk",
|
||||
"prost 0.14.3",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
@@ -10401,6 +10446,7 @@ version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
@@ -11076,11 +11122,51 @@ dependencies = [
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"wasm-streams 0.4.2",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"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 = "resb"
|
||||
version = "0.1.2"
|
||||
@@ -11123,12 +11209,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.15.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee"
|
||||
checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -11140,8 +11225,8 @@ dependencies = [
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
"process-wrap",
|
||||
"rand 0.9.3",
|
||||
"reqwest",
|
||||
"rand 0.10.1",
|
||||
"reqwest 0.13.4",
|
||||
"rmcp-macros",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
@@ -11159,9 +11244,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.15.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe"
|
||||
checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
@@ -11325,6 +11410,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"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.13"
|
||||
@@ -11662,7 +11774,7 @@ checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"native-tls",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"sentry-actix",
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
@@ -12115,6 +12227,16 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -14105,6 +14227,19 @@ 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"
|
||||
@@ -14214,7 +14349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"ndk-context",
|
||||
"objc2",
|
||||
@@ -14263,7 +14398,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"fs2",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"scratch",
|
||||
"semver",
|
||||
"zip 0.6.6",
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
|
||||
regex = "1.12.3"
|
||||
regex-lite = "0.1.8"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
rmcp = { version = "0.15.0", default-features = false }
|
||||
rmcp = { version = "1.7.0", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"ring",
|
||||
|
||||
@@ -690,6 +690,7 @@ impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitatio
|
||||
Self {
|
||||
action: value.action.into(),
|
||||
content: value.content,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,6 +1699,7 @@ fn mcp_server_elicitation_response_round_trips_rmcp_result() {
|
||||
content: Some(json!({
|
||||
"confirmed": true,
|
||||
})),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone());
|
||||
|
||||
@@ -1432,10 +1432,7 @@ impl AppsServerControl {
|
||||
|
||||
impl ServerHandler for AppListMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
|
||||
@@ -288,11 +288,8 @@ struct ResourceAppsMcpServer;
|
||||
|
||||
impl ServerHandler for ResourceAppsMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
capabilities: ServerCapabilities::builder().enable_resources().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_resources().build())
|
||||
.with_protocol_version(ProtocolVersion::V_2025_06_18)
|
||||
}
|
||||
|
||||
async fn read_resource(
|
||||
@@ -308,21 +305,19 @@ impl ServerHandler for ResourceAppsMcpServer {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![
|
||||
ResourceContents::TextResourceContents {
|
||||
uri: TEST_RESOURCE_URI.to_string(),
|
||||
mime_type: Some("text/markdown".to_string()),
|
||||
text: TEST_RESOURCE_TEXT.to_string(),
|
||||
meta: None,
|
||||
},
|
||||
ResourceContents::BlobResourceContents {
|
||||
uri: TEST_BLOB_RESOURCE_URI.to_string(),
|
||||
mime_type: Some("application/octet-stream".to_string()),
|
||||
blob: TEST_RESOURCE_BLOB.to_string(),
|
||||
meta: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
Ok(ReadResourceResult::new(vec![
|
||||
ResourceContents::TextResourceContents {
|
||||
uri: TEST_RESOURCE_URI.to_string(),
|
||||
mime_type: Some("text/markdown".to_string()),
|
||||
text: TEST_RESOURCE_TEXT.to_string(),
|
||||
meta: None,
|
||||
},
|
||||
ResourceContents::BlobResourceContents {
|
||||
uri: TEST_BLOB_RESOURCE_URI.to_string(),
|
||||
mime_type: Some("application/octet-stream".to_string()),
|
||||
blob: TEST_RESOURCE_BLOB.to_string(),
|
||||
meta: None,
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,11 +308,8 @@ struct ElicitationAppsMcpServer;
|
||||
|
||||
impl ServerHandler for ElicitationAppsMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: rmcp::model::ProtocolVersion::V_2025_06_18,
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_protocol_version(rmcp::model::ProtocolVersion::V_2025_06_18)
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
|
||||
@@ -205,10 +205,7 @@ struct McpStatusServer {
|
||||
|
||||
impl ServerHandler for McpStatusServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
@@ -244,13 +241,12 @@ struct SlowInventoryServer {
|
||||
|
||||
impl ServerHandler for SlowInventoryServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder()
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_resources()
|
||||
.build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
|
||||
@@ -537,10 +537,7 @@ struct ToolAppsMcpServer;
|
||||
|
||||
impl ServerHandler for ToolAppsMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
|
||||
@@ -1122,10 +1122,7 @@ struct PluginInstallMcpServer {
|
||||
|
||||
impl ServerHandler for PluginInstallMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
|
||||
@@ -1768,10 +1768,7 @@ struct PluginReadMcpServer {
|
||||
|
||||
impl ServerHandler for PluginReadMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
|
||||
@@ -510,9 +510,8 @@ impl McpConnectionManager {
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(next.clone()),
|
||||
let params = cursor.as_ref().map(|next| {
|
||||
PaginatedRequestParams::default().with_cursor(Some(next.clone()))
|
||||
});
|
||||
let response = match client.list_resources(params, timeout).await {
|
||||
Ok(result) => result,
|
||||
@@ -576,9 +575,8 @@ impl McpConnectionManager {
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(next.clone()),
|
||||
let params = cursor.as_ref().map(|next| {
|
||||
PaginatedRequestParams::default().with_cursor(Some(next.clone()))
|
||||
});
|
||||
let response = match client.list_resource_templates(params, timeout).await {
|
||||
Ok(result) => result,
|
||||
|
||||
@@ -44,17 +44,11 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: server_name.to_string(),
|
||||
namespace_description: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(format!("Test tool: {tool_name}").into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
tool: Tool::new(
|
||||
tool_name.to_string(),
|
||||
format!("Test tool: {tool_name}"),
|
||||
Arc::new(JsonObject::default()),
|
||||
),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -300,13 +300,7 @@ pub async fn read_mcp_resource(
|
||||
.await;
|
||||
|
||||
let result = manager
|
||||
.read_resource(
|
||||
server,
|
||||
ReadResourceRequestParams {
|
||||
meta: None,
|
||||
uri: uri.to_string(),
|
||||
},
|
||||
)
|
||||
.read_resource(server, ReadResourceRequestParams::new(uri))
|
||||
.await;
|
||||
cancel_token.cancel();
|
||||
result
|
||||
|
||||
@@ -470,26 +470,13 @@ async fn start_server_task(
|
||||
codex_apps_tools_cache_context,
|
||||
client_elicitation_capability,
|
||||
} = params;
|
||||
let params = InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
extensions: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: Some(client_elicitation_capability),
|
||||
tasks: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
title: Some("Codex".into()),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
};
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
capabilities.elicitation = Some(client_elicitation_capability);
|
||||
let params = InitializeRequestParams::new(
|
||||
capabilities,
|
||||
Implementation::new("codex-mcp-client", env!("CARGO_PKG_VERSION")).with_title("Codex"),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_06_18);
|
||||
|
||||
let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event);
|
||||
|
||||
@@ -647,32 +634,27 @@ mod tests {
|
||||
use rmcp::model::Meta;
|
||||
|
||||
fn tool_with_connector_meta() -> RmcpTool {
|
||||
RmcpTool {
|
||||
name: "capture_file_upload".to_string().into(),
|
||||
title: None,
|
||||
description: Some("test tool".to_string().into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: Some(Meta(
|
||||
serde_json::json!({
|
||||
"connector_id": "connector_gmail",
|
||||
"connector_name": "Gmail",
|
||||
"connector_display_name": "Gmail",
|
||||
"connector_description": "Mail connector",
|
||||
"connectorDescription": "Mail connector",
|
||||
"connectorFutureField": "future connector metadata",
|
||||
"CONNECTOR_UPPERCASE": "uppercase connector metadata",
|
||||
"openai/fileParams": ["file"],
|
||||
"custom": "kept"
|
||||
})
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.clone(),
|
||||
)),
|
||||
}
|
||||
RmcpTool::new(
|
||||
"capture_file_upload",
|
||||
"test tool",
|
||||
Arc::new(JsonObject::default()),
|
||||
)
|
||||
.with_meta(Meta(
|
||||
serde_json::json!({
|
||||
"connector_id": "connector_gmail",
|
||||
"connector_name": "Gmail",
|
||||
"connector_display_name": "Gmail",
|
||||
"connector_description": "Mail connector",
|
||||
"connectorDescription": "Mail connector",
|
||||
"connectorFutureField": "future connector metadata",
|
||||
"CONNECTOR_UPPERCASE": "uppercase connector metadata",
|
||||
"openai/fileParams": ["file"],
|
||||
"custom": "kept"
|
||||
})
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -540,13 +540,7 @@ impl CodexThread {
|
||||
let result = self
|
||||
.codex
|
||||
.session
|
||||
.read_resource(
|
||||
server,
|
||||
ReadResourceRequestParams {
|
||||
meta: None,
|
||||
uri: uri.to_string(),
|
||||
},
|
||||
)
|
||||
.read_resource(server, ReadResourceRequestParams::new(uri))
|
||||
.await?;
|
||||
|
||||
Ok(serde_json::to_value(result)?)
|
||||
|
||||
@@ -31,13 +31,13 @@ use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn annotations(destructive_hint: Option<bool>, open_world_hint: Option<bool>) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
ToolAnnotations::from_raw(
|
||||
/*title*/ None,
|
||||
/*read_only_hint*/ None,
|
||||
destructive_hint,
|
||||
idempotent_hint: None,
|
||||
/*idempotent_hint*/ None,
|
||||
open_world_hint,
|
||||
read_only_hint: None,
|
||||
title: None,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn app(id: &str) -> AppInfo {
|
||||
@@ -63,17 +63,7 @@ fn plugin_names(names: &[&str]) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn test_tool_definition(tool_name: &str) -> Tool {
|
||||
Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
Tool::new_with_raw(tool_name.to_string(), None, Arc::new(JsonObject::default()))
|
||||
}
|
||||
|
||||
fn codex_app_tool(
|
||||
@@ -243,17 +233,11 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: Some("Plan events".to_string()),
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event".into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
tool: Tool::new(
|
||||
"calendar_create_event",
|
||||
"Create a calendar event",
|
||||
Arc::new(JsonObject::default()),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -56,13 +56,13 @@ fn annotations(
|
||||
destructive: Option<bool>,
|
||||
open_world: Option<bool>,
|
||||
) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
destructive_hint: destructive,
|
||||
idempotent_hint: None,
|
||||
open_world_hint: open_world,
|
||||
read_only_hint: read_only,
|
||||
title: None,
|
||||
}
|
||||
ToolAnnotations::from_raw(
|
||||
/*title*/ None,
|
||||
read_only,
|
||||
destructive,
|
||||
/*idempotent_hint*/ None,
|
||||
open_world,
|
||||
)
|
||||
}
|
||||
|
||||
fn approval_metadata(
|
||||
|
||||
@@ -46,17 +46,11 @@ fn make_mcp_tool(
|
||||
callable_name: callable_name.to_string(),
|
||||
callable_namespace: callable_namespace.to_string(),
|
||||
namespace_description: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(format!("Test tool: {tool_name}").into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
tool: Tool::new(
|
||||
tool_name.to_string(),
|
||||
format!("Test tool: {tool_name}"),
|
||||
Arc::new(JsonObject::default()),
|
||||
),
|
||||
connector_id: connector_id.map(str::to_string),
|
||||
connector_name: connector_name.map(str::to_string),
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -522,19 +522,13 @@ mod tests {
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: callable_namespace.to_string(),
|
||||
namespace_description: None,
|
||||
tool: rmcp::model::Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: Arc::new(rmcp::model::object(serde_json::json!({
|
||||
tool: rmcp::model::Tool::new_with_raw(
|
||||
tool_name.to_string(),
|
||||
None,
|
||||
Arc::new(rmcp::model::object(serde_json::json!({
|
||||
"type": "object",
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -82,10 +82,9 @@ impl ToolExecutor<ToolInvocation> for ListMcpResourceTemplatesHandler {
|
||||
|
||||
let payload_result: Result<ListResourceTemplatesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
let params = cursor.clone().map(|value| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(value),
|
||||
});
|
||||
let params = cursor
|
||||
.clone()
|
||||
.map(|value| PaginatedRequestParams::default().with_cursor(Some(value)));
|
||||
let result = session
|
||||
.list_resource_templates(&server_name, params)
|
||||
.await
|
||||
|
||||
@@ -82,10 +82,9 @@ impl ToolExecutor<ToolInvocation> for ListMcpResourcesHandler {
|
||||
|
||||
let payload_result: Result<ListResourcesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
let params = cursor.clone().map(|value| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(value),
|
||||
});
|
||||
let params = cursor
|
||||
.clone()
|
||||
.map(|value| PaginatedRequestParams::default().with_cursor(Some(value)));
|
||||
let result = session
|
||||
.list_resources(&server_name, params)
|
||||
.await
|
||||
|
||||
@@ -78,13 +78,7 @@ impl ToolExecutor<ToolInvocation> for ReadMcpResourceHandler {
|
||||
|
||||
let payload_result: Result<ReadResourcePayload, FunctionCallError> = async {
|
||||
let result = session
|
||||
.read_resource(
|
||||
&server,
|
||||
ReadResourceRequestParams {
|
||||
meta: None,
|
||||
uri: uri.clone(),
|
||||
},
|
||||
)
|
||||
.read_resource(&server, ReadResourceRequestParams::new(uri.clone()))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!("resources/read failed: {err:#}"))
|
||||
|
||||
@@ -50,11 +50,10 @@ fn tool_info() -> ToolInfo {
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__calendar__".to_string(),
|
||||
namespace_description: Some("Plan events.".to_string()),
|
||||
tool: rmcp::model::Tool {
|
||||
name: "createEvent".to_string().into(),
|
||||
title: Some("Create event".to_string()),
|
||||
description: Some("Create a calendar event.".to_string().into()),
|
||||
input_schema: Arc::new(rmcp::model::object(json!({
|
||||
tool: rmcp::model::Tool::new(
|
||||
"createEvent",
|
||||
"Create a calendar event.",
|
||||
Arc::new(rmcp::model::object(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start_time": { "type": "string" },
|
||||
@@ -62,12 +61,8 @@ fn tool_info() -> ToolInfo {
|
||||
},
|
||||
"additionalProperties": false
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.with_title("Create event"),
|
||||
connector_id: None,
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: vec![" Calendar plugin ".to_string(), " ".to_string()],
|
||||
|
||||
@@ -258,21 +258,15 @@ mod tests {
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: format!("mcp__{server_name}"),
|
||||
namespace_description: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(format!("{description_prefix} desktop tool").into()),
|
||||
input_schema: Arc::new(rmcp::model::object(serde_json::json!({
|
||||
tool: Tool::new(
|
||||
tool_name.to_string(),
|
||||
format!("{description_prefix} desktop tool"),
|
||||
Arc::new(rmcp::model::object(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -306,19 +306,13 @@ fn mcp_tool_info(
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: callable_namespace.to_string(),
|
||||
namespace_description: None,
|
||||
tool: rmcp::model::Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: Some("Test MCP tool".to_string().into()),
|
||||
input_schema: Arc::new(rmcp::model::object(json!({
|
||||
tool: rmcp::model::Tool::new(
|
||||
tool_name.to_string(),
|
||||
"Test MCP tool",
|
||||
Arc::new(rmcp::model::object(json!({
|
||||
"type": "object",
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -302,21 +302,15 @@ fn mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo {
|
||||
callable_name: name.to_string(),
|
||||
callable_namespace: namespace.to_string(),
|
||||
namespace_description: Some(format!("Tools from {server}.")),
|
||||
tool: rmcp::model::Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(format!("{name} test tool").into()),
|
||||
input_schema: Arc::new(rmcp::model::object(json!({
|
||||
tool: rmcp::model::Tool::new(
|
||||
name.to_string(),
|
||||
format!("{name} test tool"),
|
||||
Arc::new(rmcp::model::object(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
|
||||
@@ -116,20 +116,13 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
|
||||
let input_schema = create_tool_input_schema(schema, "Codex tool schema should serialize");
|
||||
|
||||
Tool {
|
||||
name: "codex".into(),
|
||||
title: Some("Codex".to_string()),
|
||||
Tool::new(
|
||||
"codex",
|
||||
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
|
||||
input_schema,
|
||||
output_schema: Some(codex_tool_output_schema()),
|
||||
description: Some(
|
||||
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct."
|
||||
.into(),
|
||||
),
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
)
|
||||
.with_title("Codex")
|
||||
.with_raw_output_schema(codex_tool_output_schema())
|
||||
}
|
||||
|
||||
fn codex_tool_output_schema() -> Arc<JsonObject> {
|
||||
@@ -242,19 +235,13 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
|
||||
|
||||
let input_schema = create_tool_input_schema(schema, "Codex reply tool schema should serialize");
|
||||
|
||||
Tool {
|
||||
name: "codex-reply".into(),
|
||||
title: Some("Codex Reply".to_string()),
|
||||
Tool::new(
|
||||
"codex-reply",
|
||||
"Continue a Codex conversation by providing the thread id and prompt.",
|
||||
input_schema,
|
||||
output_schema: Some(codex_tool_output_schema()),
|
||||
description: Some(
|
||||
"Continue a Codex conversation by providing the thread id and prompt.".into(),
|
||||
),
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
)
|
||||
.with_title("Codex Reply")
|
||||
.with_raw_output_schema(codex_tool_output_schema())
|
||||
}
|
||||
|
||||
fn create_tool_input_schema(
|
||||
|
||||
@@ -44,12 +44,10 @@ pub(crate) fn create_call_tool_result_with_thread_id(
|
||||
"threadId": thread_id,
|
||||
"content": content_text,
|
||||
});
|
||||
CallToolResult {
|
||||
content,
|
||||
is_error,
|
||||
structured_content: Some(structured_content),
|
||||
meta: None,
|
||||
}
|
||||
let mut result = CallToolResult::success(content);
|
||||
result.is_error = is_error;
|
||||
result.structured_content = Some(structured_content);
|
||||
result
|
||||
}
|
||||
|
||||
/// Run a complete Codex session and stream events back to the client.
|
||||
@@ -71,12 +69,9 @@ pub async fn run_codex_tool_session(
|
||||
} = match thread_manager.start_thread(config.clone()).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![Content::text(format!("Failed to start Codex session: {e}"))],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![Content::text(format!(
|
||||
"Failed to start Codex session: {e}"
|
||||
))]);
|
||||
outgoing.send_response(id.clone(), result).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ use rmcp::model::JsonRpcRequest;
|
||||
use rmcp::model::JsonRpcResponse;
|
||||
use rmcp::model::RequestId;
|
||||
use rmcp::model::ServerCapabilities;
|
||||
use rmcp::model::ToolsCapability;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task;
|
||||
@@ -218,14 +217,8 @@ impl MessageProcessor {
|
||||
*suffix = Some(user_agent_suffix);
|
||||
}
|
||||
|
||||
let server_info = Implementation {
|
||||
name: "codex-mcp-server".to_string(),
|
||||
title: Some("Codex".to_string()),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
};
|
||||
let server_info =
|
||||
Implementation::new("codex-mcp-server", env!("CARGO_PKG_VERSION")).with_title("Codex");
|
||||
|
||||
// Preserve Codex's existing non-spec `serverInfo.user_agent` field.
|
||||
let mut server_info_value = match serde_json::to_value(&server_info) {
|
||||
@@ -247,17 +240,14 @@ impl MessageProcessor {
|
||||
obj.insert("user_agent".to_string(), json!(get_codex_user_agent()));
|
||||
}
|
||||
|
||||
let mut result_value = match serde_json::to_value(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
tools: Some(ToolsCapability {
|
||||
list_changed: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
instructions: None,
|
||||
protocol_version: params.protocol_version.clone(),
|
||||
server_info,
|
||||
}) {
|
||||
let capabilities = ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed()
|
||||
.build();
|
||||
let result = InitializeResult::new(capabilities)
|
||||
.with_protocol_version(params.protocol_version.clone())
|
||||
.with_server_info(server_info);
|
||||
let mut result_value = match serde_json::to_value(result) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
self.outgoing
|
||||
@@ -345,12 +335,9 @@ impl MessageProcessor {
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(format!("Unknown tool '{name}'"))],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(format!(
|
||||
"Unknown tool '{name}'"
|
||||
))]);
|
||||
self.outgoing.send_response(id, result).await;
|
||||
}
|
||||
}
|
||||
@@ -367,40 +354,25 @@ impl MessageProcessor {
|
||||
Ok(tool_cfg) => match tool_cfg.into_config(self.arg0_paths.clone()).await {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(format!(
|
||||
"Failed to load Codex configuration from overrides: {e}"
|
||||
))],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(
|
||||
format!("Failed to load Codex configuration from overrides: {e}"),
|
||||
)]);
|
||||
self.outgoing.send_response(id, result).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse configuration for Codex tool: {e}"
|
||||
))],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse configuration for Codex tool: {e}"
|
||||
))]);
|
||||
self.outgoing.send_response(id, result).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(
|
||||
"Missing arguments for codex tool-call; the `prompt` field is required.",
|
||||
)],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(
|
||||
"Missing arguments for codex tool-call; the `prompt` field is required.",
|
||||
)]);
|
||||
self.outgoing.send_response(id, result).await;
|
||||
return;
|
||||
}
|
||||
@@ -441,14 +413,9 @@ impl MessageProcessor {
|
||||
Ok(params) => params,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse Codex tool call reply parameters: {e}");
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse configuration for Codex tool: {e}"
|
||||
))],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse configuration for Codex tool: {e}"
|
||||
))]);
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
@@ -457,14 +424,9 @@ impl MessageProcessor {
|
||||
tracing::error!(
|
||||
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required."
|
||||
);
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(
|
||||
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.",
|
||||
)],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(
|
||||
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.",
|
||||
)]);
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
@@ -474,14 +436,9 @@ impl MessageProcessor {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse thread_id: {e}");
|
||||
let result = CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse thread_id: {e}"
|
||||
))],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
let result = CallToolResult::error(vec![rmcp::model::Content::text(format!(
|
||||
"Failed to parse thread_id: {e}"
|
||||
))]);
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl From<OutgoingMessage> for OutgoingJsonRpcMessage {
|
||||
}
|
||||
Error(OutgoingError { id, error }) => JsonRpcMessage::Error(JsonRpcError {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id,
|
||||
id: Some(id),
|
||||
error,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -114,31 +114,18 @@ impl McpProcess {
|
||||
pub async fn initialize(&mut self) -> anyhow::Result<()> {
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let params = InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
elicitation: Some(ElicitationCapability {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
}),
|
||||
experimental: None,
|
||||
extensions: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
tasks: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "elicitation test".into(),
|
||||
title: Some("Elicitation Test".into()),
|
||||
version: "0.0.0".into(),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||
};
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
capabilities.elicitation = Some(ElicitationCapability {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
});
|
||||
let params = InitializeRequestParams::new(
|
||||
capabilities,
|
||||
Implementation::new("elicitation test", "0.0.0").with_title("Elicitation Test"),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_03_26);
|
||||
let params_value = serde_json::to_value(params)?;
|
||||
|
||||
self.send_jsonrpc_message(JsonRpcMessage::Request(JsonRpcRequest {
|
||||
@@ -203,15 +190,12 @@ impl McpProcess {
|
||||
&mut self,
|
||||
params: CodexToolCallParam,
|
||||
) -> anyhow::Result<i64> {
|
||||
let codex_tool_call_params = CallToolRequestParams {
|
||||
meta: None,
|
||||
name: "codex".into(),
|
||||
arguments: Some(match serde_json::to_value(params)? {
|
||||
let codex_tool_call_params = CallToolRequestParams::new("codex").with_arguments(
|
||||
match serde_json::to_value(params)? {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => unreachable!("params serialize to object"),
|
||||
}),
|
||||
task: None,
|
||||
};
|
||||
},
|
||||
);
|
||||
self.send_request(
|
||||
"tools/call",
|
||||
Some(serde_json::to_value(codex_tool_call_params)?),
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
anyhow = "1"
|
||||
axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"json",
|
||||
"tokio",
|
||||
] }
|
||||
base64 = { workspace = true }
|
||||
@@ -26,10 +27,10 @@ bytes = { 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", default-features = false, features = [
|
||||
"json",
|
||||
"stream",
|
||||
"rustls-tls",
|
||||
"rustls",
|
||||
] }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"auth",
|
||||
|
||||
@@ -79,13 +79,12 @@ struct EchoArgs {
|
||||
|
||||
impl ServerHandler for TestToolServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder()
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed()
|
||||
.build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
@@ -130,12 +129,9 @@ impl ServerHandler for TestToolServer {
|
||||
"env": env_snapshot.get(env_name),
|
||||
});
|
||||
|
||||
Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(structured_content),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
let mut result = CallToolResult::success(Vec::new());
|
||||
result.structured_content = Some(structured_content);
|
||||
Ok(result)
|
||||
}
|
||||
other => Err(McpError::invalid_params(
|
||||
format!("unknown tool: {other}"),
|
||||
|
||||
@@ -407,11 +407,8 @@ impl ServerHandler for TestToolServer {
|
||||
JsonObject::new(),
|
||||
)]));
|
||||
|
||||
ServerInfo {
|
||||
instructions: Some("Use these tools to exercise the rmcp test server.".to_string()),
|
||||
capabilities,
|
||||
..ServerInfo::default()
|
||||
}
|
||||
ServerInfo::new(capabilities)
|
||||
.with_instructions("Use these tools to exercise the rmcp test server.")
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
@@ -462,14 +459,14 @@ impl ServerHandler for TestToolServer {
|
||||
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> Result<ReadResourceResult, McpError> {
|
||||
if uri == MEMO_URI {
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::TextResourceContents {
|
||||
Ok(ReadResourceResult::new(vec![
|
||||
ResourceContents::TextResourceContents {
|
||||
uri,
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: Self::memo_text().to_string(),
|
||||
meta: None,
|
||||
}],
|
||||
})
|
||||
},
|
||||
]))
|
||||
} else {
|
||||
Err(McpError::resource_not_found(
|
||||
"resource_not_found",
|
||||
@@ -484,22 +481,14 @@ impl ServerHandler for TestToolServer {
|
||||
context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
match request.name.as_ref() {
|
||||
"sandbox_meta" => Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(serde_json::Value::Object(context.meta.0)),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
"sandbox_meta" => Ok(Self::structured_result(serde_json::Value::Object(
|
||||
context.meta.0,
|
||||
))),
|
||||
"cwd" => {
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.map_err(|err| McpError::internal_error(err.to_string(), None))?;
|
||||
Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(json!({ "cwd": cwd })),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
Ok(Self::structured_result(json!({ "cwd": cwd })))
|
||||
}
|
||||
"echo" | "echo-tool" => {
|
||||
let args: EchoArgs = match request.arguments {
|
||||
@@ -522,12 +511,7 @@ impl ServerHandler for TestToolServer {
|
||||
"env": env_snapshot.get(env_name),
|
||||
});
|
||||
|
||||
Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(structured_content),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
Ok(Self::structured_result(structured_content))
|
||||
}
|
||||
"image" => {
|
||||
// Read a data URL (e.g. data:image/png;base64,AAA...) from env and convert to
|
||||
@@ -677,12 +661,13 @@ impl TestToolServer {
|
||||
sleep(Duration::from_millis(delay)).await;
|
||||
}
|
||||
|
||||
Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(json!({ "result": "ok" })),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
Ok(Self::structured_result(json!({ "result": "ok" })))
|
||||
}
|
||||
|
||||
fn structured_result(value: serde_json::Value) -> CallToolResult {
|
||||
let mut result = CallToolResult::success(Vec::new());
|
||||
result.structured_content = Some(value);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ use axum::body::Body;
|
||||
use axum::extract::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::http::Method;
|
||||
use axum::http::Request;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::header::AUTHORIZATION;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::header::HOST;
|
||||
use axum::http::header::WWW_AUTHENTICATE;
|
||||
use axum::middleware;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
@@ -72,12 +74,17 @@ struct SessionFailureState {
|
||||
struct ArmedFailure {
|
||||
status: StatusCode,
|
||||
remaining: usize,
|
||||
/// Raw `WWW-Authenticate` challenge header field values returned with the failure.
|
||||
www_authenticate_headers: Vec<HeaderValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ArmSessionPostFailureRequest {
|
||||
status: u16,
|
||||
remaining: usize,
|
||||
/// Raw `WWW-Authenticate` challenge header field values to add to the failure.
|
||||
#[serde(default)]
|
||||
www_authenticate_headers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -174,14 +181,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
impl ServerHandler for TestToolServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder()
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed()
|
||||
.enable_resources()
|
||||
.build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
@@ -232,14 +238,14 @@ impl ServerHandler for TestToolServer {
|
||||
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> Result<ReadResourceResult, McpError> {
|
||||
if uri == MEMO_URI {
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::TextResourceContents {
|
||||
Ok(ReadResourceResult::new(vec![
|
||||
ResourceContents::TextResourceContents {
|
||||
uri,
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: Self::memo_text().to_string(),
|
||||
meta: None,
|
||||
}],
|
||||
})
|
||||
},
|
||||
]))
|
||||
} else {
|
||||
Err(McpError::resource_not_found(
|
||||
"resource_not_found",
|
||||
@@ -274,12 +280,9 @@ impl ServerHandler for TestToolServer {
|
||||
"env": env_snapshot.get("MCP_TEST_VALUE"),
|
||||
});
|
||||
|
||||
Ok(CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(structured_content),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
let mut result = CallToolResult::success(Vec::new());
|
||||
result.structured_content = Some(structured_content);
|
||||
Ok(result)
|
||||
}
|
||||
other => Err(McpError::invalid_params(
|
||||
format!("unknown tool: {other}"),
|
||||
@@ -405,12 +408,18 @@ async fn arm_session_post_failure(
|
||||
Json(request): Json<ArmSessionPostFailureRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let status = StatusCode::from_u16(request.status).map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
let www_authenticate_headers = request
|
||||
.www_authenticate_headers
|
||||
.into_iter()
|
||||
.map(|value| HeaderValue::from_str(&value).map_err(|_| StatusCode::BAD_REQUEST))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let armed_failure = if request.remaining == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ArmedFailure {
|
||||
status,
|
||||
remaining: request.remaining,
|
||||
www_authenticate_headers,
|
||||
})
|
||||
};
|
||||
*state.armed_failure.lock().await = armed_failure;
|
||||
@@ -436,6 +445,7 @@ async fn fail_session_post_when_armed(
|
||||
{
|
||||
failure.remaining -= 1;
|
||||
let status = failure.status;
|
||||
let www_authenticate_headers = failure.www_authenticate_headers.clone();
|
||||
if failure.remaining == 0 {
|
||||
*armed_failure = None;
|
||||
}
|
||||
@@ -443,6 +453,11 @@ async fn fail_session_post_when_armed(
|
||||
"forced session failure with status {status}"
|
||||
)));
|
||||
*response.status_mut() = status;
|
||||
for www_authenticate_header in www_authenticate_headers {
|
||||
response
|
||||
.headers_mut()
|
||||
.append(WWW_AUTHENTICATE, www_authenticate_header);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! - a local HTTP client that issues requests from the orchestrator, or
|
||||
//! - a remote HTTP client that forwards requests to the remote runtime
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -29,12 +30,17 @@ use reqwest::header::HeaderName;
|
||||
use rmcp::model::ClientJsonRpcMessage;
|
||||
use rmcp::model::ServerJsonRpcMessage;
|
||||
use rmcp::transport::streamable_http_client::AuthRequiredError;
|
||||
use rmcp::transport::streamable_http_client::InsufficientScopeError;
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpClient;
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpError;
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpPostResponse;
|
||||
use sse_stream::Sse;
|
||||
use sse_stream::SseStream;
|
||||
|
||||
mod www_authenticate;
|
||||
|
||||
use self::www_authenticate::insufficient_scope_challenge;
|
||||
|
||||
const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
|
||||
const JSON_MIME_TYPE: &str = "application/json";
|
||||
const HEADER_SESSION_ID: &str = "Mcp-Session-Id";
|
||||
@@ -80,8 +86,10 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
message: ClientJsonRpcMessage,
|
||||
session_id: Option<Arc<str>>,
|
||||
auth_token: Option<String>,
|
||||
custom_headers: HashMap<HeaderName, reqwest::header::HeaderValue>,
|
||||
) -> std::result::Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
headers.extend(custom_headers);
|
||||
self.add_auth_headers(&mut headers);
|
||||
insert_header(
|
||||
&mut headers,
|
||||
@@ -137,9 +145,19 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
&& let Some(header) =
|
||||
response_header(&response.headers, reqwest::header::WWW_AUTHENTICATE)
|
||||
{
|
||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
||||
www_authenticate_header: header,
|
||||
}));
|
||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError::new(
|
||||
header,
|
||||
)));
|
||||
}
|
||||
if response.status == StatusCode::FORBIDDEN.as_u16()
|
||||
&& let Some(challenge) = insufficient_scope_challenge(&response.headers)
|
||||
{
|
||||
return Err(StreamableHttpError::InsufficientScope(
|
||||
InsufficientScopeError::new(
|
||||
challenge.www_authenticate_header,
|
||||
challenge.required_scope,
|
||||
),
|
||||
));
|
||||
}
|
||||
if matches!(
|
||||
StatusCode::from_u16(response.status).ok(),
|
||||
@@ -177,8 +195,10 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
uri: Arc<str>,
|
||||
session: Arc<str>,
|
||||
auth_token: Option<String>,
|
||||
custom_headers: HashMap<HeaderName, reqwest::header::HeaderValue>,
|
||||
) -> std::result::Result<(), StreamableHttpError<Self::Error>> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
headers.extend(custom_headers);
|
||||
self.add_auth_headers(&mut headers);
|
||||
if let Some(auth_token) = auth_token {
|
||||
insert_header(
|
||||
@@ -227,11 +247,13 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
session_id: Arc<str>,
|
||||
last_event_id: Option<String>,
|
||||
auth_token: Option<String>,
|
||||
custom_headers: HashMap<HeaderName, reqwest::header::HeaderValue>,
|
||||
) -> std::result::Result<
|
||||
BoxStream<'static, std::result::Result<Sse, sse_stream::Error>>,
|
||||
StreamableHttpError<Self::Error>,
|
||||
> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
headers.extend(custom_headers);
|
||||
self.add_auth_headers(&mut headers);
|
||||
insert_header(
|
||||
&mut headers,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
use codex_exec_server::HttpHeader;
|
||||
use reqwest::header::WWW_AUTHENTICATE;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct InsufficientScopeChallenge {
|
||||
pub(super) www_authenticate_header: String,
|
||||
pub(super) required_scope: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct BearerInsufficientScope {
|
||||
required_scope: Option<String>,
|
||||
}
|
||||
|
||||
type AuthParameter<'a> = (&'a str, Option<String>);
|
||||
type ChallengeStart<'a> = (&'a str, Option<AuthParameter<'a>>);
|
||||
|
||||
#[derive(Default)]
|
||||
enum Parameter {
|
||||
#[default]
|
||||
Missing,
|
||||
Value(String),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BearerChallenge {
|
||||
error: Parameter,
|
||||
scope: Parameter,
|
||||
}
|
||||
|
||||
impl BearerChallenge {
|
||||
fn add_parameter(&mut self, name: &str, value: Option<String>) {
|
||||
let parameter = if name.eq_ignore_ascii_case("error") {
|
||||
&mut self.error
|
||||
} else if name.eq_ignore_ascii_case("scope") {
|
||||
&mut self.scope
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
*parameter = match (&*parameter, value) {
|
||||
(Parameter::Missing, Some(value)) => Parameter::Value(value),
|
||||
(Parameter::Missing, None) | (Parameter::Value(_), _) | (Parameter::Invalid, _) => {
|
||||
Parameter::Invalid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn into_insufficient_scope(self) -> Option<BearerInsufficientScope> {
|
||||
match self.error {
|
||||
Parameter::Value(error) if error == "insufficient_scope" => {
|
||||
Some(BearerInsufficientScope {
|
||||
required_scope: match self.scope {
|
||||
Parameter::Value(scope) if valid_scope(&scope) => Some(scope),
|
||||
Parameter::Missing | Parameter::Value(_) | Parameter::Invalid => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
Parameter::Missing | Parameter::Value(_) | Parameter::Invalid => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds a Bearer insufficient-scope challenge among all `WWW-Authenticate`
|
||||
/// response header field values.
|
||||
pub(super) fn insufficient_scope_challenge(
|
||||
headers: &[HttpHeader],
|
||||
) -> Option<InsufficientScopeChallenge> {
|
||||
headers
|
||||
.iter()
|
||||
.filter(|header| header.name.eq_ignore_ascii_case(WWW_AUTHENTICATE.as_str()))
|
||||
.find_map(|header| {
|
||||
parse_bearer_insufficient_scope(&header.value).map(|challenge| {
|
||||
InsufficientScopeChallenge {
|
||||
www_authenticate_header: header.value.clone(),
|
||||
required_scope: challenge.required_scope,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a Bearer `WWW-Authenticate` challenge with an `insufficient_scope`
|
||||
/// error and extracts its optional required scope.
|
||||
///
|
||||
/// RFC 9110 section 11.2 defines challenge parameters as `auth-param` values
|
||||
/// whose values are either `token` or `quoted-string`. Quoted strings use HTTP
|
||||
/// syntax rather than JSON: section 5.6.4 requires recipients to replace each
|
||||
/// `quoted-pair` with its escaped octet.
|
||||
///
|
||||
/// RFC 6750 section 3 permits `scope` in the Bearer challenge at most once.
|
||||
/// After HTTP quoted-string processing, each scope token can contain `%x21`,
|
||||
/// `%x23-5B`, or `%x5D-7E`, with `%x20` separating multiple tokens. Therefore
|
||||
/// returned scopes cannot contain `"` or `\`, even when those characters occur
|
||||
/// in the header encoding.
|
||||
///
|
||||
/// RMCP has related parsing logic, but it is private to that crate.
|
||||
fn parse_bearer_insufficient_scope(header: &str) -> Option<BearerInsufficientScope> {
|
||||
let segments = split_unquoted_segments(header)?;
|
||||
let mut bearer_challenge: Option<BearerChallenge> = None;
|
||||
|
||||
for segment in segments {
|
||||
if let Some((name, value)) = parse_auth_param(segment) {
|
||||
if let Some(challenge) = bearer_challenge.as_mut() {
|
||||
challenge.add_parameter(name, value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(challenge) = bearer_challenge
|
||||
.take()
|
||||
.and_then(BearerChallenge::into_insufficient_scope)
|
||||
{
|
||||
return Some(challenge);
|
||||
}
|
||||
|
||||
let (scheme, parameter) = parse_challenge_start(segment)?;
|
||||
if scheme.eq_ignore_ascii_case("Bearer") {
|
||||
let mut challenge = BearerChallenge::default();
|
||||
if let Some((name, value)) = parameter {
|
||||
challenge.add_parameter(name, value);
|
||||
}
|
||||
bearer_challenge = Some(challenge);
|
||||
}
|
||||
}
|
||||
|
||||
bearer_challenge.and_then(BearerChallenge::into_insufficient_scope)
|
||||
}
|
||||
|
||||
fn parse_challenge_start(segment: &str) -> Option<ChallengeStart<'_>> {
|
||||
let segment = segment.trim();
|
||||
let parameter_start = segment.find(char::is_whitespace);
|
||||
let (scheme, parameter) = match parameter_start {
|
||||
Some(parameter_start) => (
|
||||
&segment[..parameter_start],
|
||||
parse_auth_param(&segment[parameter_start..]),
|
||||
),
|
||||
None => (segment, None),
|
||||
};
|
||||
|
||||
is_http_token(scheme).then_some((scheme, parameter))
|
||||
}
|
||||
|
||||
fn parse_auth_param(segment: &str) -> Option<AuthParameter<'_>> {
|
||||
let (name, value) = segment.trim().split_once('=')?;
|
||||
let name = name.trim();
|
||||
is_http_token(name).then_some((name, parse_auth_param_value(value.trim())))
|
||||
}
|
||||
|
||||
fn parse_auth_param_value(value: &str) -> Option<String> {
|
||||
if let Some(quoted_value) = value.strip_prefix('"') {
|
||||
let quoted_value = quoted_value.strip_suffix('"')?;
|
||||
let mut decoded = String::with_capacity(quoted_value.len());
|
||||
let mut characters = quoted_value.chars();
|
||||
while let Some(character) = characters.next() {
|
||||
if character == '\\' {
|
||||
decoded.push(characters.next()?);
|
||||
} else {
|
||||
decoded.push(character);
|
||||
}
|
||||
}
|
||||
Some(decoded)
|
||||
} else {
|
||||
is_http_token(value).then(|| value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn split_unquoted_segments(header: &str) -> Option<Vec<&str>> {
|
||||
let mut segments = Vec::new();
|
||||
let mut segment_start = 0;
|
||||
let mut in_quotes = false;
|
||||
let mut escaped = false;
|
||||
|
||||
for (position, character) in header.char_indices() {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
match character {
|
||||
'\\' if in_quotes => escaped = true,
|
||||
'"' => in_quotes = !in_quotes,
|
||||
',' | ';' if !in_quotes => {
|
||||
segments.push(&header[segment_start..position]);
|
||||
segment_start = position + character.len_utf8();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if in_quotes || escaped {
|
||||
None
|
||||
} else {
|
||||
segments.push(&header[segment_start..]);
|
||||
Some(segments)
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_scope(scope: &str) -> bool {
|
||||
scope.split(' ').all(|token| {
|
||||
!token.is_empty()
|
||||
&& token
|
||||
.bytes()
|
||||
.all(|byte| matches!(byte, b'!' | b'#'..=b'[' | b']'..=b'~'))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_http_token(value: &str) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.bytes().all(|byte| {
|
||||
byte.is_ascii_alphanumeric()
|
||||
|| matches!(
|
||||
byte,
|
||||
b'!' | b'#'
|
||||
| b'$'
|
||||
| b'%'
|
||||
| b'&'
|
||||
| b'\''
|
||||
| b'*'
|
||||
| b'+'
|
||||
| b'-'
|
||||
| b'.'
|
||||
| b'^'
|
||||
| b'_'
|
||||
| b'`'
|
||||
| b'|'
|
||||
| b'~'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "www_authenticate_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,124 @@
|
||||
use codex_exec_server::HttpHeader;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::BearerInsufficientScope;
|
||||
use super::InsufficientScopeChallenge;
|
||||
use super::insufficient_scope_challenge;
|
||||
use super::parse_bearer_insufficient_scope;
|
||||
|
||||
#[test]
|
||||
fn extracts_scope_from_bearer_insufficient_scope_challenges() {
|
||||
let cases = [
|
||||
(
|
||||
r#"Bearer error="insufficient_scope", scope="files:read files:write""#,
|
||||
"files:read files:write",
|
||||
),
|
||||
(
|
||||
r#"Bearer error="insufficient_scope", ScOpE = "files:read""#,
|
||||
"files:read",
|
||||
),
|
||||
(
|
||||
r#"Bearer scope="read:data", error="insufficient_scope""#,
|
||||
"read:data",
|
||||
),
|
||||
(r#"Bearer error="insufficient_scope", scope=read"#, "read"),
|
||||
(
|
||||
r#"Bearer error="insufficient_scope", scope="files:read\ files:write""#,
|
||||
"files:read files:write",
|
||||
),
|
||||
(
|
||||
r#"Bearer error="insufficient_scope", error_description="request scope=admin, not \"root\"", scope="files:read""#,
|
||||
"files:read",
|
||||
),
|
||||
(
|
||||
r#"Basic realm="example", Bearer error="insufficient_scope", scope="files:read""#,
|
||||
"files:read",
|
||||
),
|
||||
(
|
||||
r#"Newauth scope="wrong", Bearer error="insufficient_scope", scope="files:read""#,
|
||||
"files:read",
|
||||
),
|
||||
];
|
||||
|
||||
for (header, expected_scope) in cases {
|
||||
assert_eq!(
|
||||
parse_bearer_insufficient_scope(header),
|
||||
Some(BearerInsufficientScope {
|
||||
required_scope: Some(expected_scope.to_string()),
|
||||
}),
|
||||
"header: {header}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_treat_other_bearer_errors_as_insufficient_scope() {
|
||||
assert_eq!(
|
||||
parse_bearer_insufficient_scope(r#"Bearer error="invalid_token", scope="files:read""#),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_or_ambiguous_scope_parameters() {
|
||||
let cases = [
|
||||
r#"Bearer error="insufficient_scope", scope="#,
|
||||
r#"Bearer error="insufficient_scope", scope="read\"write""#,
|
||||
r#"Bearer error="insufficient_scope", scope="read\\write""#,
|
||||
r#"Bearer error="insufficient_scope", scope="read write""#,
|
||||
r#"Bearer error="insufficient_scope", scope=read:data"#,
|
||||
r#"Bearer error="insufficient_scope", scope=files:read files:write"#,
|
||||
r#"Bearer error="insufficient_scope", scope=read=value"#,
|
||||
r#"Bearer error="insufficient_scope", scope="read", scope="write""#,
|
||||
];
|
||||
|
||||
for header in cases {
|
||||
assert_eq!(
|
||||
parse_bearer_insufficient_scope(header),
|
||||
Some(BearerInsufficientScope {
|
||||
required_scope: None,
|
||||
}),
|
||||
"header: {header}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_scope_text_outside_a_scope_parameter() {
|
||||
let cases = [
|
||||
r#"Bearer error_description="request scope=admin""#,
|
||||
r#"Bearer resource_scope="admin""#,
|
||||
r#"Bearer "scope=admin""#,
|
||||
r#"Bearer error_description="unterminated scope=admin"#,
|
||||
];
|
||||
|
||||
for header in cases {
|
||||
assert_eq!(
|
||||
parse_bearer_insufficient_scope(header),
|
||||
None,
|
||||
"header: {header}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_bearer_challenge_from_a_later_www_authenticate_field_value() {
|
||||
let headers = vec![
|
||||
HttpHeader {
|
||||
name: "www-authenticate".to_string(),
|
||||
value: r#"Basic realm="example""#.to_string(),
|
||||
},
|
||||
HttpHeader {
|
||||
name: "WWW-Authenticate".to_string(),
|
||||
value: r#"Bearer error="insufficient_scope", scope="files:read""#.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
insufficient_scope_challenge(&headers),
|
||||
Some(InsufficientScopeChallenge {
|
||||
www_authenticate_header: headers[1].value.clone(),
|
||||
required_scope: Some("files:read".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -21,12 +21,12 @@ use anyhow::Error;
|
||||
use anyhow::Result;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use oauth2::AccessToken;
|
||||
use oauth2::EmptyExtraTokenFields;
|
||||
use oauth2::RefreshToken;
|
||||
use oauth2::Scope;
|
||||
use oauth2::TokenResponse;
|
||||
use oauth2::basic::BasicTokenType;
|
||||
use rmcp::transport::auth::OAuthTokenResponse;
|
||||
use rmcp::transport::auth::VendorExtraTokenFields;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -403,7 +403,7 @@ fn load_oauth_tokens_from_file(server_name: &str, url: &str) -> Result<Option<St
|
||||
let mut token_response = OAuthTokenResponse::new(
|
||||
AccessToken::new(entry.access_token.clone()),
|
||||
BasicTokenType::Bearer,
|
||||
EmptyExtraTokenFields {},
|
||||
VendorExtraTokenFields::default(),
|
||||
);
|
||||
|
||||
if let Some(refresh) = entry.refresh_token.clone() {
|
||||
@@ -878,8 +878,8 @@ mod tests {
|
||||
);
|
||||
assert_eq!(actual_response.scopes(), expected_response.scopes());
|
||||
assert_eq!(
|
||||
actual_response.extra_fields(),
|
||||
expected_response.extra_fields()
|
||||
actual_response.extra_fields().0,
|
||||
expected_response.extra_fields().0
|
||||
);
|
||||
assert_eq!(
|
||||
actual_response.expires_in().is_some(),
|
||||
@@ -891,7 +891,7 @@ mod tests {
|
||||
let mut response = OAuthTokenResponse::new(
|
||||
AccessToken::new("access-token".to_string()),
|
||||
BasicTokenType::Bearer,
|
||||
EmptyExtraTokenFields {},
|
||||
VendorExtraTokenFields::default(),
|
||||
);
|
||||
response.set_refresh_token(Some(RefreshToken::new("refresh-token".to_string())));
|
||||
response.set_scopes(Some(vec![
|
||||
|
||||
@@ -621,19 +621,15 @@ async fn start_authorization(
|
||||
auth_manager.with_client(http_client)?;
|
||||
let metadata = auth_manager.discover_metadata().await?;
|
||||
auth_manager.set_metadata(metadata);
|
||||
auth_manager.configure_client(OAuthClientConfig {
|
||||
client_id: oauth_client_id.to_string(),
|
||||
client_secret: None,
|
||||
scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
})?;
|
||||
auth_manager.configure_client(
|
||||
OAuthClientConfig::new(oauth_client_id, redirect_uri)
|
||||
.with_scopes(scopes.iter().map(|scope| (*scope).to_string()).collect()),
|
||||
)?;
|
||||
let auth_url = auth_manager.get_authorization_url(scopes).await?;
|
||||
|
||||
Ok(OAuthState::Session(AuthorizationSession {
|
||||
auth_manager,
|
||||
auth_url,
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
}))
|
||||
Ok(OAuthState::Session(
|
||||
AuthorizationSession::for_scope_upgrade(auth_manager, auth_url, redirect_uri),
|
||||
))
|
||||
}
|
||||
|
||||
fn append_query_param(url: &str, key: &str, value: Option<&str>) -> String {
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::maybe_build_rustls_client_config_with_custom_ca;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_exec_server::HttpClient;
|
||||
use futures::FutureExt;
|
||||
@@ -249,6 +249,7 @@ impl From<ElicitationResponse> for CreateElicitationResult {
|
||||
Self {
|
||||
action: value.action,
|
||||
content: value.content,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,29 +569,22 @@ impl RmcpClient {
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let rmcp_params = CallToolRequestParams {
|
||||
meta: None,
|
||||
name: name.into(),
|
||||
arguments,
|
||||
task: None,
|
||||
};
|
||||
let mut rmcp_params = CallToolRequestParams::new(name);
|
||||
rmcp_params.arguments = arguments;
|
||||
let result = self
|
||||
.run_service_operation("tools/call", timeout, move |service| {
|
||||
let rmcp_params = rmcp_params.clone();
|
||||
let meta = meta.clone();
|
||||
async move {
|
||||
let mut options = rmcp::service::PeerRequestOptions::no_options();
|
||||
options.meta = meta;
|
||||
let result = service
|
||||
.peer()
|
||||
.send_request_with_option(
|
||||
ClientRequest::CallToolRequest(rmcp::model::CallToolRequest {
|
||||
method: Default::default(),
|
||||
params: rmcp_params,
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
rmcp::service::PeerRequestOptions {
|
||||
timeout: None,
|
||||
meta,
|
||||
},
|
||||
ClientRequest::CallToolRequest(rmcp::model::CallToolRequest::new(
|
||||
rmcp_params,
|
||||
)),
|
||||
options,
|
||||
)
|
||||
.await?
|
||||
.await_response()
|
||||
@@ -1019,8 +1013,11 @@ async fn create_oauth_transport_and_runtime(
|
||||
StreamableHttpClientTransport<AuthClient<StreamableHttpClientAdapter>>,
|
||||
OAuthPersistor,
|
||||
)> {
|
||||
let builder = apply_default_headers(reqwest::Client::builder(), &default_headers);
|
||||
let oauth_metadata_client = build_reqwest_client_with_custom_ca(builder)?;
|
||||
let mut builder = apply_default_headers(reqwest::Client::builder(), &default_headers);
|
||||
if let Some(tls_config) = maybe_build_rustls_client_config_with_custom_ca()? {
|
||||
builder = builder.tls_backend_preconfigured(tls_config.as_ref().clone());
|
||||
}
|
||||
let oauth_metadata_client = builder.build()?;
|
||||
// TODO(aibrahim): teach OAuth bootstrap and refresh to use the same
|
||||
// shared HTTP client abstraction instead of always creating the local
|
||||
// reqwest metadata client here.
|
||||
@@ -1037,7 +1034,7 @@ async fn create_oauth_transport_and_runtime(
|
||||
let manager = match oauth_state {
|
||||
OAuthState::Authorized(manager) => manager,
|
||||
OAuthState::Unauthorized(manager) => manager,
|
||||
OAuthState::Session(_) | OAuthState::AuthorizedHttpClient(_) => {
|
||||
_ => {
|
||||
return Err(anyhow!("unexpected OAuth state during client setup"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,26 +25,11 @@ fn stdio_server_bin() -> Result<std::path::PathBuf> {
|
||||
}
|
||||
|
||||
fn init_params() -> InitializeRequestParams {
|
||||
InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
extensions: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: None,
|
||||
tasks: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-test".into(),
|
||||
version: "0.0.0-test".into(),
|
||||
title: Some("Codex rmcp shutdown test".into()),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
}
|
||||
InitializeRequestParams::new(
|
||||
ClientCapabilities::default(),
|
||||
Implementation::new("codex-test", "0.0.0-test").with_title("Codex rmcp shutdown test"),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_06_18)
|
||||
}
|
||||
|
||||
fn process_exists(pid: u32) -> bool {
|
||||
|
||||
@@ -28,31 +28,18 @@ fn stdio_server_bin() -> Result<PathBuf, CargoBinError> {
|
||||
}
|
||||
|
||||
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 resource test".into()),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
}
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
capabilities.elicitation = Some(ElicitationCapability {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
});
|
||||
InitializeRequestParams::new(
|
||||
capabilities,
|
||||
Implementation::new("codex-test", "0.0.0-test").with_title("Codex rmcp resource test"),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_06_18)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
@@ -132,10 +119,7 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
|
||||
|
||||
let read = client
|
||||
.read_resource(
|
||||
ReadResourceRequestParams {
|
||||
meta: None,
|
||||
uri: RESOURCE_URI.to_string(),
|
||||
},
|
||||
ReadResourceRequestParams::new(RESOURCE_URI),
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -16,7 +16,13 @@ async fn streamable_http_404_session_expiry_recovers_and_retries_once() -> anyho
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(&base_url, /*status*/ 404, /*remaining*/ 1).await?;
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 404,
|
||||
/*remaining*/ 1,
|
||||
/*www_authenticate_headers*/ &[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let recovered = call_echo_tool(&client, "recovered").await?;
|
||||
assert_eq!(recovered, expected_echo_result("recovered"));
|
||||
@@ -32,7 +38,13 @@ async fn streamable_http_401_does_not_trigger_recovery() -> anyhow::Result<()> {
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(&base_url, /*status*/ 401, /*remaining*/ 2).await?;
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 401,
|
||||
/*remaining*/ 2,
|
||||
/*www_authenticate_headers*/ &[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let first_error = call_echo_tool(&client, "unauthorized").await.unwrap_err();
|
||||
assert!(first_error.to_string().contains("401"));
|
||||
@@ -45,6 +57,61 @@ async fn streamable_http_401_does_not_trigger_recovery() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_403_scope_challenge_returns_insufficient_scope() -> anyhow::Result<()> {
|
||||
let (_server, base_url) = spawn_streamable_http_server().await?;
|
||||
let client = create_client(&base_url).await?;
|
||||
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 403,
|
||||
/*remaining*/ 1,
|
||||
/*www_authenticate_headers*/
|
||||
&[r#"Bearer error="insufficient_scope", scope="files:read files:write""#],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error = call_echo_tool(&client, "forbidden").await.unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("Insufficient scope"),
|
||||
"expected insufficient-scope transport error, got: {error:#}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_403_finds_bearer_challenge_in_later_header_value() -> anyhow::Result<()> {
|
||||
let (_server, base_url) = spawn_streamable_http_server().await?;
|
||||
let client = create_client(&base_url).await?;
|
||||
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 403,
|
||||
/*remaining*/ 1,
|
||||
/*www_authenticate_headers*/
|
||||
&[
|
||||
r#"Basic realm="example""#,
|
||||
r#"Bearer error="insufficient_scope", scope="files:read""#,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error = call_echo_tool(&client, "forbidden").await.unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("Insufficient scope"),
|
||||
"expected insufficient-scope transport error, got: {error:#}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_404_recovery_only_retries_once() -> anyhow::Result<()> {
|
||||
let (_server, base_url) = spawn_streamable_http_server().await?;
|
||||
@@ -53,7 +120,13 @@ async fn streamable_http_404_recovery_only_retries_once() -> anyhow::Result<()>
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(&base_url, /*status*/ 404, /*remaining*/ 2).await?;
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 404,
|
||||
/*remaining*/ 2,
|
||||
/*www_authenticate_headers*/ &[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error = call_echo_tool(&client, "double-404").await.unwrap_err();
|
||||
assert!(
|
||||
@@ -77,7 +150,13 @@ async fn streamable_http_non_session_failure_does_not_trigger_recovery() -> anyh
|
||||
let warmup = call_echo_tool(&client, "warmup").await?;
|
||||
assert_eq!(warmup, expected_echo_result("warmup"));
|
||||
|
||||
arm_session_post_failure(&base_url, /*status*/ 500, /*remaining*/ 2).await?;
|
||||
arm_session_post_failure(
|
||||
&base_url,
|
||||
/*status*/ 500,
|
||||
/*remaining*/ 2,
|
||||
/*www_authenticate_headers*/ &[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let first_error = call_echo_tool(&client, "server-error").await.unwrap_err();
|
||||
assert!(first_error.to_string().contains("500"));
|
||||
|
||||
@@ -50,43 +50,27 @@ fn streamable_http_server_bin() -> Result<PathBuf, CargoBinError> {
|
||||
}
|
||||
|
||||
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 recovery test".into()),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
}
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
capabilities.elicitation = Some(ElicitationCapability {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
});
|
||||
InitializeRequestParams::new(
|
||||
capabilities,
|
||||
Implementation::new("codex-test", "0.0.0-test").with_title("Codex rmcp recovery test"),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_06_18)
|
||||
}
|
||||
|
||||
pub(crate) fn expected_echo_result(message: &str) -> CallToolResult {
|
||||
CallToolResult {
|
||||
content: Vec::new(),
|
||||
structured_content: Some(json!({
|
||||
"echo": format!("ECHOING: {message}"),
|
||||
"env": null,
|
||||
})),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}
|
||||
let mut result = CallToolResult::success(Vec::new());
|
||||
result.structured_content = Some(json!({
|
||||
"echo": format!("ECHOING: {message}"),
|
||||
"env": null,
|
||||
}));
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn create_client(base_url: &str) -> anyhow::Result<RmcpClient> {
|
||||
@@ -178,12 +162,14 @@ pub(crate) async fn arm_session_post_failure(
|
||||
base_url: &str,
|
||||
status: u16,
|
||||
remaining: usize,
|
||||
www_authenticate_headers: &[&str],
|
||||
) -> anyhow::Result<()> {
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{base_url}{SESSION_POST_FAILURE_CONTROL_PATH}"))
|
||||
.json(&json!({
|
||||
"status": status,
|
||||
"remaining": remaining,
|
||||
"www_authenticate_headers": www_authenticate_headers,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -6,17 +6,11 @@ use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
||||
rmcp::model::Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(description.to_string().into()),
|
||||
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
rmcp::model::Tool::new(
|
||||
name.to_string(),
|
||||
description.to_string(),
|
||||
std::sync::Arc::new(rmcp::model::object(input_schema)),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -87,11 +87,10 @@ fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() {
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
|
||||
let tool = rmcp::model::Tool {
|
||||
name: "lookup_order".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Look up an order".to_string().into()),
|
||||
input_schema: std::sync::Arc::new(rmcp::model::object(json!({
|
||||
let tool = rmcp::model::Tool::new(
|
||||
"lookup_order",
|
||||
"Look up an order",
|
||||
std::sync::Arc::new(rmcp::model::object(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order_id": {"type": "string"}
|
||||
@@ -99,12 +98,7 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
|
||||
"required": ["order_id"],
|
||||
"additionalProperties": false,
|
||||
}))),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
};
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mcp_tool_to_deferred_responses_api_tool(
|
||||
|
||||
@@ -201,17 +201,11 @@ fn convert_fixture_tool(
|
||||
.as_object()
|
||||
.unwrap_or_else(|| panic!("{name} input_schema should be an object"))
|
||||
.clone();
|
||||
let tool = rmcp::model::Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(fixture_tool.description.clone().into()),
|
||||
input_schema: Arc::new(input_schema),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
};
|
||||
let tool = rmcp::model::Tool::new(
|
||||
name.to_string(),
|
||||
fixture_tool.description.clone(),
|
||||
Arc::new(input_schema),
|
||||
);
|
||||
|
||||
mcp_tool_to_responses_api_tool(&ToolName::namespaced(&fixture.source, name), &tool)
|
||||
.unwrap_or_else(|err| panic!("convert {name} from {}: {err}", fixture.source))
|
||||
|
||||
Reference in New Issue
Block a user