mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Add MCP 2026-07-28 discovery support (#35724)
## What changed - Add an opt-in `mcp_2026_07_28` protocol mode while preserving the legacy lifecycle by default. - Negotiate the new protocol over streamable HTTP with `server/discover`, including bounded responses, redirect protection, and fallback only when a response establishes that the endpoint is legacy-only. - Require stdio servers to opt in with `CODEX_MCP_PROTOCOL_VERSION=2026-07-28`, and add a bounded local stdio transport for the modern lifecycle. - Consume paginated tool, resource, and resource-template catalogs in modern mode, reject repeated cursors, and retain discovered server identity. - Reconnect reusable MCP clients when their selected protocol mode changes. ## Testing - Cover HTTP JSON and SSE discovery, legacy fallback and rejection cases, redirects, retries, response limits, and pagination. - Cover local and executor stdio discovery, protocol markers, message limits, and legacy compatibility. GitOrigin-RevId: f6a78816e127d2a482292d63b91c8384f1595903
This commit is contained in:
1 parent
85c6da1c79
commit
be2e4afcd7
28 files changed
+2891
-105
No files matched your search
@@ -0,0 +1,107 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::collect_paginated;
|
||||
|
||||
#[tokio::test]
|
||||
async fn collects_all_pages_including_an_empty_cursor() {
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&requests);
|
||||
|
||||
let pages = collect_paginated("tools/list", /*overall_timeout*/ None, move |params| {
|
||||
let observed = Arc::clone(&observed);
|
||||
async move {
|
||||
let cursor = params.and_then(|params| params.cursor);
|
||||
observed.lock().expect("request lock").push(cursor.clone());
|
||||
match cursor.as_deref() {
|
||||
None => Ok((vec!["first"], Some(String::new()))),
|
||||
Some("") => Ok((vec!["second"], Some("last".to_string()))),
|
||||
Some("last") => Ok((vec!["third"], None)),
|
||||
Some(cursor) => Err(anyhow!("unexpected cursor: {cursor}")),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("paginated request succeeds");
|
||||
|
||||
assert_eq!(pages, vec!["first", "second", "third"]);
|
||||
assert_eq!(
|
||||
*requests.lock().expect("request lock"),
|
||||
vec![None, Some(String::new()), Some("last".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_nonconsecutive_repeated_cursors() {
|
||||
let error = collect_paginated(
|
||||
"resources/list",
|
||||
/*overall_timeout*/ None,
|
||||
|params| async move {
|
||||
let cursor = params.and_then(|params| params.cursor);
|
||||
let next = match cursor.as_deref() {
|
||||
None => "first",
|
||||
Some("first") => "second",
|
||||
Some("second") => "first",
|
||||
Some(cursor) => return Err(anyhow!("unexpected cursor: {cursor}")),
|
||||
};
|
||||
Ok((Vec::<()>::new(), Some(next.to_string())))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("a repeated cursor must fail");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"resources/list returned a repeated pagination cursor"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forwards_page_failures() {
|
||||
let error = collect_paginated(
|
||||
"resources/templates/list",
|
||||
/*overall_timeout*/ None,
|
||||
|_params| async { Err::<(Vec<()>, Option<String>), _>(anyhow!("page failed")) },
|
||||
)
|
||||
.await
|
||||
.expect_err("a page error must fail");
|
||||
|
||||
assert_eq!(error.to_string(), "page failed");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn applies_one_timeout_across_individually_timely_pages() {
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&requests);
|
||||
|
||||
let error = collect_paginated("tools/list", Some(Duration::from_secs(5)), move |params| {
|
||||
let observed = Arc::clone(&observed);
|
||||
async move {
|
||||
let cursor = params.and_then(|params| params.cursor);
|
||||
observed.lock().expect("request lock").push(cursor.clone());
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
match cursor.as_deref() {
|
||||
None => Ok((vec!["first"], Some("second".to_string()))),
|
||||
Some("second") => Ok((vec!["second"], Some("third".to_string()))),
|
||||
Some("third") => Ok((vec!["third"], None)),
|
||||
Some(cursor) => Err(anyhow!("unexpected cursor: {cursor}")),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect_err("the combined page duration must exceed the shared timeout");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"tools/list pagination timed out after 5s"
|
||||
);
|
||||
assert_eq!(
|
||||
*requests.lock().expect("request lock"),
|
||||
vec![None, Some("second".to_string()), Some("third".to_string())]
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user