Merge branch 'wt/review-002-con17-mcp-pagination'

This commit is contained in:
glm-5.3-flash committed 2026-08-30 19:58:43 +00:00
commit 76c9e7e178
2 files changed
+129 -7

No files matched your search

+64
View File
@@ -168,6 +168,37 @@ impl ServerHandler for PagingServer {
}
}
/// A hostile paging `tools/list` server (review-002 CON-14): always
/// responds with the same single tool and `next_cursor: Some("a")` — the
/// cursor cycles and never clears. The importer must bound the walk and
/// fail with a clean `DiscoveryFailed` rather than hang forever.
struct CyclingCursorServer;
impl ServerHandler for CyclingCursorServer {
fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl std::future::Future<Output = Result<ListToolsResult, rmcp::ErrorData>>
+ rmcp::service::MaybeSendFuture
+ '_ {
let tools = vec![Tool::new_with_raw(
"hostile",
Some("served on every page, forever".into()),
Arc::new(serde_json::Map::new()),
)];
std::future::ready(Ok(ListToolsResult {
meta: None,
next_cursor: Some("a".to_string()),
tools,
}))
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo::default()
}
}
async fn spawn_server_for<S: ServerHandler + 'static>(
server: impl Fn() -> Result<S, std::io::Error> + Send + Sync + 'static,
) -> (String, tokio::task::JoinHandle<()>) {
@@ -316,6 +347,39 @@ async fn import_follows_tools_list_pagination() {
);
}
#[tokio::test]
async fn import_cycling_cursor_fails_bounded_with_discovery_failed() {
let (endpoint, _handle) = spawn_server_for(|| Ok(CyclingCursorServer)).await;
let adapter = FromMCP::new(endpoint, "x");
let started = Instant::now();
let result = tokio::time::timeout(Duration::from_secs(10), adapter.import()).await;
let elapsed = started.elapsed();
let outcome = result.expect("import must terminate, not hang (CON-14)");
match outcome {
Ok(_) => panic!("expected Err for a cycling-cursor server"),
Err(alkcall::client::AdapterError::DiscoveryFailed { message }) => {
assert!(
message.contains("pagination exceeded budget"),
"error must name the budget, got: {message}"
);
assert!(
message.contains("page"),
"error must name pages fetched, got: {message}"
);
assert!(
message.contains("tool"),
"error must name tools accumulated, got: {message}"
);
}
Err(other) => panic!("expected DiscoveryFailed, got {other}"),
}
assert!(
elapsed < Duration::from_secs(10),
"bounded walk must trip the page cap well inside the outer guard, took {elapsed:?}"
);
}
#[tokio::test]
async fn import_refuses_tool_name_containing_slash() {
struct SlashToolServer;