use std::time::Duration;
use tower_mcp::client::{ChannelTransport, McpClient};
use tower_mcp::extract::RawArgs;
use tower_mcp::protocol::TaskStatus;
use tower_mcp::{CallToolResult, McpRouter, TaskSupportMode, ToolBuilder};
fn task_router() -> McpRouter {
McpRouter::new()
.server_info("tasks-test", "1.0.0")
.tool(
ToolBuilder::new("compute")
.description("Finishes quickly with a result")
.task_support(TaskSupportMode::Optional)
.extractor_handler((), |RawArgs(_): RawArgs| async move {
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(CallToolResult::text("the answer is 42"))
})
.build(),
)
.tool(
ToolBuilder::new("forever")
.description("Runs until cancelled")
.task_support(TaskSupportMode::Optional)
.extractor_handler((), |RawArgs(_): RawArgs| async move {
tokio::time::sleep(Duration::from_secs(60)).await;
Ok(CallToolResult::text("never happens"))
})
.build(),
)
}
#[tokio::test]
async fn task_augmented_call_polls_and_retrieves_result() {
let client = McpClient::connect(ChannelTransport::new(task_router()))
.await
.expect("connect");
client
.initialize("test", "1.0.0")
.await
.expect("initialize");
let created = client
.call_tool_as_task("compute", serde_json::json!({}), Some(60_000))
.await
.expect("task-augmented call");
assert!(!created.task.task_id.is_empty());
assert_eq!(created.task.status, TaskStatus::Working);
assert!(
created.task.result.is_none(),
"no result payload while working"
);
let polled = client
.task_get(&created.task.task_id)
.await
.expect("tasks/get");
assert_eq!(polled.task_id, created.task.task_id);
let done = tokio::time::timeout(
Duration::from_secs(5),
client.task_wait(&created.task.task_id),
)
.await
.expect("task_wait timed out")
.expect("task_wait");
assert_eq!(done.status, TaskStatus::Completed);
let result = done.result.expect("completed task must carry its result");
match &result.content[0] {
tower_mcp::protocol::Content::Text { text, .. } => {
assert_eq!(text, "the answer is 42")
}
other => panic!("expected text content, got {other:?}"),
}
assert!(done.error.is_none());
}
#[tokio::test]
async fn task_cancel_resolves_to_terminal_state() {
let client = McpClient::connect(ChannelTransport::new(task_router()))
.await
.expect("connect");
client
.initialize("test", "1.0.0")
.await
.expect("initialize");
let created = client
.call_tool_as_task("forever", serde_json::json!({}), None)
.await
.expect("task-augmented call");
client
.task_cancel(&created.task.task_id, Some("test cancellation".into()))
.await
.expect("tasks/cancel");
let done = tokio::time::timeout(
Duration::from_secs(5),
client.task_wait(&created.task.task_id),
)
.await
.expect("task_wait timed out")
.expect("task_wait");
assert_eq!(done.status, TaskStatus::Cancelled);
assert!(done.result.is_none());
}
#[tokio::test]
async fn task_get_unknown_id_errors() {
let client = McpClient::connect(ChannelTransport::new(task_router()))
.await
.expect("connect");
client
.initialize("test", "1.0.0")
.await
.expect("initialize");
let err = client
.task_get("task-does-not-exist")
.await
.expect_err("unknown task id must error");
assert!(err.to_string().contains("not found"), "got: {err}");
}
#[tokio::test]
async fn task_update_sends_typed_responses_and_tolerates_stale_keys() {
use tower_mcp::protocol::{ElicitAction, ElicitResult, InputResponse, InputResponses};
let client = McpClient::connect(ChannelTransport::new(task_router()))
.await
.expect("connect");
client
.initialize("test", "1.0.0")
.await
.expect("initialize");
let created = client
.call_tool_as_task("compute", serde_json::json!({}), None)
.await
.expect("task-augmented call");
let mut responses = InputResponses::new();
responses.insert(
"approval".to_string(),
InputResponse::Elicit(ElicitResult {
action: ElicitAction::Accept,
content: None,
meta: None,
}),
);
client
.task_update(&created.task.task_id, responses.clone())
.await
.expect("tasks/update with no outstanding requests must be accepted");
client
.task_update(&created.task.task_id, responses)
.await
.expect("replayed tasks/update must be accepted");
client
.task_update(&created.task.task_id, InputResponses::new())
.await
.expect("empty tasks/update must be accepted");
let done = tokio::time::timeout(
Duration::from_secs(5),
client.task_wait(&created.task.task_id),
)
.await
.expect("task_wait timed out")
.expect("task_wait");
assert_eq!(done.status, TaskStatus::Completed);
let err = client
.task_update("task-does-not-exist", InputResponses::new())
.await
.expect_err("unknown task id must error");
assert!(err.to_string().contains("not found"), "got: {err}");
}
#[cfg(feature = "stateless")]
#[tokio::test]
async fn final_call_tool_transparently_completes_server_created_task() {
let client = McpClient::builder()
.protocol_support(tower_mcp::ProtocolSupport::try_new(["2026-07-28"]).unwrap())
.with_tasks()
.connect_simple(ChannelTransport::new(task_router().with_tasks()))
.await
.expect("connect");
client.discover("test", "1.0.0").await.expect("discover");
let result = tokio::time::timeout(
Duration::from_secs(5),
client.call_tool("compute", serde_json::json!({})),
)
.await
.expect("call_tool timed out")
.expect("call_tool");
assert_eq!(result.first_text(), Some("the answer is 42"));
}
#[cfg(feature = "stateless")]
#[tokio::test]
async fn final_task_aware_call_exposes_direct_lifecycle() {
use tower_mcp::client::TaskAwareCallToolOutcome;
let client = McpClient::builder()
.protocol_support(tower_mcp::ProtocolSupport::try_new(["2026-07-28"]).unwrap())
.with_tasks()
.connect_simple(ChannelTransport::new(task_router().with_tasks()))
.await
.expect("connect");
client.discover("test", "1.0.0").await.expect("discover");
let outcome = client
.call_tool_once_task_aware("compute", serde_json::json!({}), None, None)
.await
.expect("tools/call");
let TaskAwareCallToolOutcome::Task(created) = outcome else {
panic!("expected server-created task")
};
let done = tokio::time::timeout(
Duration::from_secs(5),
client.task_wait(&created.task.metadata.task_id),
)
.await
.expect("task_wait timed out")
.expect("task_wait");
assert_eq!(done.status, TaskStatus::Completed);
assert_eq!(
done.result.as_ref().and_then(CallToolResult::first_text),
Some("the answer is 42")
);
}