rust-sdk/examples/clients/src/progress_client.rs
Teddy Tennant 3075dc9152
fix(model): deprecate ServerInfo and ClientInfo aliases (#1156)
* fix(model): deprecate ServerInfo and ClientInfo aliases

Those names collide with the protocol's serverInfo/clientInfo fields,
which are only Implementation identity. Prefer InitializeResult and
InitializeRequestParams. Handler trait signatures, the tool_handler
macro output, tests, examples and docs now use the canonical types; the
aliases remain as deprecated shims for the rest of 3.x.

Fixes #1082

* fix(model): keep public signatures on the deprecated aliases

The public API check compares rendered signatures, and rustdoc records a
type alias by the name it was written with. Respelling
`ServerHandler::get_info`, `ClientHandler::get_info`,
`DiscoverResult::from_server_info` and `impl ClientHandler for ClientInfo`
onto `InitializeResult`/`InitializeRequestParams` therefore reads as
changed public items even though the aliases resolve to those exact types
and nothing downstream breaks.

Put those four signatures back on the aliases so the published API is
byte-identical, and note why at the alias definitions. The deprecation
still steers new code, and the macro output, tests, examples and docs
keep using the canonical names.

* fix(model): migrate leftover ClientInfo test sites

Two tests landed on main after the alias deprecation and still named
ClientInfo. Clippy -D warnings and the no-local feature test job both
failed on them.
2026-09-12 12:11:30 -04:00

324 lines
9.6 KiB
Rust

use std::{
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use anyhow::Result;
use clap::{Parser, ValueEnum};
use rmcp::{
ClientHandler, ServiceExt,
model::{
CallToolRequestParams, ClientCapabilities, Implementation, InitializeRequestParams,
ProgressNotificationParam,
},
service::{NotificationContext, RoleClient},
transport::{StreamableHttpClientTransport, TokioChildProcess},
};
use tokio::{process::Command, time::sleep};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Debug, Clone, ValueEnum)]
enum TransportType {
Stdio,
Http,
All,
}
#[derive(Parser)]
#[command(name = "progress-test-client")]
#[command(about = "RMCP Progress Notification Test Client")]
struct Args {
/// Transport type to test
#[arg(short, long, value_enum, default_value_t = TransportType::Stdio)]
transport: TransportType,
/// Number of records to process
#[arg(short, long, default_value_t = 10)]
records: u32,
/// HTTP server URL
#[arg(long, default_value = "http://127.0.0.1:8001/mcp")]
http_url: String,
}
#[derive(Debug, Clone)]
struct ProgressTracker {
start_time: Instant,
progress_count: Arc<Mutex<u32>>,
}
impl ProgressTracker {
fn new() -> Self {
Self {
start_time: Instant::now(),
progress_count: Arc::new(Mutex::new(0)),
}
}
fn handle_progress(&self, params: &ProgressNotificationParam) {
if let Ok(mut count) = self.progress_count.lock() {
*count += 1;
}
let elapsed = self.start_time.elapsed();
tracing::info!(
"Progress update [{}]: {}/{} - {} (elapsed: {:.1}s)",
params.progress_token.0,
params.progress,
params.total.unwrap_or(0.0),
params.message.as_deref().unwrap_or(""),
elapsed.as_secs_f64()
);
}
fn print_summary(&self) {
let elapsed = self.start_time.elapsed();
if let Ok(count) = self.progress_count.lock() {
tracing::info!("Total progress notifications received: {}", *count);
}
tracing::info!("Total time elapsed: {:.2}s", elapsed.as_secs_f64());
}
}
// Progress-aware client handler
#[derive(Debug, Clone)]
struct ProgressAwareClient {
tracker: Arc<Mutex<Option<ProgressTracker>>>,
}
impl ProgressAwareClient {
fn new() -> Self {
Self {
tracker: Arc::new(Mutex::new(None)),
}
}
fn start_tracking(&self) {
if let Ok(mut tracker) = self.tracker.lock() {
*tracker = Some(ProgressTracker::new());
}
}
fn stop_tracking(&self) {
if let Ok(mut tracker_opt) = self.tracker.lock()
&& let Some(tracker) = tracker_opt.take()
{
tracker.print_summary();
}
}
}
impl ClientHandler for ProgressAwareClient {
async fn on_progress(
&self,
params: ProgressNotificationParam,
_context: NotificationContext<RoleClient>,
) {
if let Ok(tracker_opt) = self.tracker.lock()
&& let Some(tracker) = tracker_opt.as_ref()
{
tracker.handle_progress(&params);
}
}
fn get_info(&self) -> InitializeRequestParams {
InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new("progress-test-client", "1.0.0"),
)
}
}
async fn test_stdio_transport(records: u32) -> Result<()> {
tracing::info!("Testing STDIO Transport");
tracing::info!("================================");
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir)
.parent()
.and_then(|p| p.parent())
.ok_or_else(|| anyhow::anyhow!("Cannot find workspace root"))?;
let servers_dir = workspace_root.join("examples").join("servers");
// Start server process
let mut server_cmd = Command::new("cargo");
server_cmd
.current_dir(servers_dir)
.arg("run")
.arg("--example")
.arg("servers_progress_demo")
.arg("--")
.arg("stdio");
// Create progress-aware client handler
let client_handler = ProgressAwareClient::new();
client_handler.start_tracking();
let client_handler_clone = client_handler.clone();
let service = client_handler
.serve(TokioChildProcess::new(server_cmd)?)
.await?;
// Initialize
let server_info = service.peer_info();
if let Some(info) = server_info {
tracing::info!(
"Connected to server: {:?}",
info.server_info.as_ref().map(|server| &server.name)
);
}
// List tools
let tools = service.list_all_tools().await?;
tracing::info!(
"Available tools: {:?}",
tools.iter().map(|t| &t.name).collect::<Vec<_>>()
);
// Call stream processor tool
tracing::info!("Starting to process {} records...", records);
let tool_result = service
.call_tool(CallToolRequestParams::new("stream_processor"))
.await?;
if let Some(content) = tool_result.content.first()
&& let Some(text) = content.as_text()
{
tracing::info!("Processing completed: {}", text.text);
}
service.cancel().await?;
client_handler_clone.stop_tracking();
tracing::info!("STDIO transport test completed successfully!");
Ok(())
}
// Test HTTP transport, must run the server with `cargo run -p mcp-client-examples --example servers_progress_demo -- http` in the servers directory
async fn test_http_transport(http_url: &str, records: u32) -> Result<()> {
tracing::info!("Testing HTTP Streaming Transport");
tracing::info!("=====================================");
tracing::info!("HTTP URL: {}", http_url);
// Wait a bit for server to be ready
sleep(Duration::from_secs(1)).await;
let transport = StreamableHttpClientTransport::from_uri(http_url);
// Create progress-aware client handler
let client_handler = ProgressAwareClient::new();
client_handler.start_tracking();
let client_handler_clone = client_handler.clone();
let client = client_handler.serve(transport).await.inspect_err(|e| {
tracing::error!("HTTP client error: {:?}", e);
})?;
// Initialize
let server_info = client.peer_info();
if let Some(info) = server_info {
tracing::info!(
"Connected to server: {:?}",
info.server_info.as_ref().map(|server| &server.name)
);
}
// List tools
let tools = client.list_tools(Default::default()).await?;
tracing::info!(
"Available tools: {:?}",
tools.tools.iter().map(|t| &t.name).collect::<Vec<_>>()
);
// Call stream processor tool
tracing::info!("Starting to process {} records...", records);
let tool_result = client
.call_tool(CallToolRequestParams::new("stream_processor"))
.await?;
if let Some(content) = tool_result.content.first()
&& let Some(text) = content.as_text()
{
tracing::info!("processing completed: {}", text.text);
}
client.cancel().await?;
client_handler_clone.stop_tracking();
tracing::info!("HTTP transport test completed successfully!");
Ok(())
}
async fn run_single_test(transport_type: &TransportType, args: &Args) -> Result<bool> {
match transport_type {
TransportType::Stdio => {
test_stdio_transport(args.records).await?;
Ok(true)
}
TransportType::Http => match test_http_transport(&args.http_url, args.records).await {
Ok(_) => Ok(true),
Err(e) => {
tracing::error!("HTTP test failed: {}", e);
Ok(false)
}
},
TransportType::All => {
// This case is handled in main
Ok(true)
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
// Initialize logging
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("RMCP Progress Notification Test Client");
tracing::info!("==========================================");
match &args.transport {
TransportType::All => {
// Test all transport types
let mut results = std::collections::HashMap::new();
for transport_type in [TransportType::Stdio, TransportType::Http] {
let transport_name = format!("{:?}", transport_type).to_uppercase();
tracing::info!("\n");
match run_single_test(&transport_type, &args).await {
Ok(success) => {
results.insert(transport_name, success);
}
Err(e) => {
tracing::error!("{} test failed: {}", transport_name, e);
results.insert(transport_name, false);
}
}
sleep(Duration::from_secs(2)).await;
}
// Print summary
tracing::info!("\n==========================================");
tracing::info!("Test Results Summary");
tracing::info!("==========================================");
for (transport, success) in &results {
let status = if *success { "PASSED" } else { "FAILED" };
tracing::info!(" {:<10}: {}", transport, status);
}
}
transport_type => {
run_single_test(transport_type, &args).await?;
}
}
Ok(())
}