* feat: implement SEP-1319 Decouple Request Payload from RPC Methods * test: update tests * fix: update handler trait methods to use new types * fix: update examples * fix: correct deprecation version * fix: update wrapper macros to use new *Params type names
334 lines
9.9 KiB
Rust
334 lines
9.9 KiB
Rust
use std::{
|
|
sync::{Arc, Mutex},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use clap::{Parser, ValueEnum};
|
|
use rmcp::{
|
|
ClientHandler, ServiceExt,
|
|
model::{
|
|
CallToolRequestParams, ClientCapabilities, ClientInfo, Implementation,
|
|
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() {
|
|
if 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() {
|
|
if let Some(tracker) = tracker_opt.as_ref() {
|
|
tracker.handle_progress(¶ms);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_info(&self) -> ClientInfo {
|
|
ClientInfo {
|
|
meta: None,
|
|
protocol_version: Default::default(),
|
|
capabilities: ClientCapabilities::default(),
|
|
client_info: Implementation {
|
|
name: "progress-test-client".to_string(),
|
|
version: "1.0.0".to_string(),
|
|
..Default::default()
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
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.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 {
|
|
meta: None,
|
|
name: "stream_processor".into(),
|
|
arguments: None,
|
|
task: None,
|
|
})
|
|
.await?;
|
|
|
|
if let Some(content) = tool_result.content.first() {
|
|
if 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.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 {
|
|
meta: None,
|
|
name: "stream_processor".into(),
|
|
arguments: None,
|
|
task: None,
|
|
})
|
|
.await?;
|
|
|
|
if let Some(content) = tool_result.content.first() {
|
|
if 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(())
|
|
}
|