Files
codex/codex-rs/codex-mcp/src/pagination.rs
T
jif e4e040881a Raise the host-owned Codex Apps catalog limit (#36772)
## Why

Codex Apps tool catalogs can exceed the standard MCP limit of 2,048 items and
still need to remain available through tool search.

## What changed

- Allow up to 8,192 catalog items for host-owned `codex_apps` registrations.
- Keep the 2,048-item limit for other MCP servers, including extensions that
  register the `codex_apps` name.
- Restrict the shared Codex Apps tool cache to host-owned registrations.

## Testing

- Cover large catalogs for built-in and hosted-plugin registrations while
  verifying that another extension retains the standard limit.
- Verify that a same-named extension cannot read the host-owned Apps cache.

GitOrigin-RevId: 2265693a460e9be63c4c849169cba7324def4f49
2026-08-03 17:06:46 +00:00

85 lines
2.8 KiB
Rust

use std::collections::HashSet;
use std::future::Future;
use std::time::Duration;
use anyhow::Result;
use anyhow::anyhow;
use rmcp::model::PaginatedRequestParams;
const MAX_MCP_CATALOG_PAGES: usize = 100;
pub(crate) const MAX_MCP_CATALOG_ITEMS: usize = 2_048;
pub(crate) const MAX_CODEX_APPS_TOOL_CATALOG_ITEMS: usize = 8_192;
const MAX_MCP_PAGINATION_CURSOR_BYTES: usize = 64 * 1024;
const DEFAULT_MCP_PAGINATION_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) async fn collect_paginated<T, F, Fut>(
method: &str,
overall_timeout: Option<Duration>,
fetch: F,
) -> Result<Vec<T>>
where
F: FnMut(Option<PaginatedRequestParams>) -> Fut,
Fut: Future<Output = Result<(Vec<T>, Option<String>)>>,
{
collect_paginated_with_limit(method, overall_timeout, MAX_MCP_CATALOG_ITEMS, fetch).await
}
pub(crate) async fn collect_paginated_with_limit<T, F, Fut>(
method: &str,
overall_timeout: Option<Duration>,
max_items: usize,
mut fetch: F,
) -> Result<Vec<T>>
where
F: FnMut(Option<PaginatedRequestParams>) -> Fut,
Fut: Future<Output = Result<(Vec<T>, Option<String>)>>,
{
let collect = async {
let mut collected = Vec::new();
let mut cursor = None;
let mut seen_cursors = HashSet::new();
let mut page_count = 0;
loop {
if page_count == MAX_MCP_CATALOG_PAGES {
return Err(anyhow!(
"{method} exceeded the pagination limit of {MAX_MCP_CATALOG_PAGES} pages"
));
}
page_count += 1;
let params = cursor.as_ref().map(|next: &String| {
PaginatedRequestParams::default().with_cursor(Some(next.clone()))
});
let (items, next_cursor) = fetch(params).await?;
if items.len() > max_items.saturating_sub(collected.len()) {
return Err(anyhow!(
"{method} exceeded the catalog limit of {max_items} items"
));
}
collected.extend(items);
let Some(next_cursor) = next_cursor else {
return Ok(collected);
};
if next_cursor.len() > MAX_MCP_PAGINATION_CURSOR_BYTES {
return Err(anyhow!(
"{method} returned a pagination cursor exceeding {MAX_MCP_PAGINATION_CURSOR_BYTES} bytes"
));
}
if !seen_cursors.insert(next_cursor.clone()) {
return Err(anyhow!("{method} returned a repeated pagination cursor"));
}
cursor = Some(next_cursor);
}
};
let timeout = overall_timeout.unwrap_or(DEFAULT_MCP_PAGINATION_TIMEOUT);
tokio::time::timeout(timeout, collect)
.await
.map_err(|_| anyhow!("{method} pagination timed out after {timeout:?}"))?
}
#[cfg(test)]
#[path = "pagination_tests.rs"]
mod tests;