Merge branch 'wt/review-002-con17-mcp-pagination'
This commit is contained in:
commit
76c9e7e178
2 files changed
+129
-7
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user