use async_trait::async_trait;
use pmcp::error::Result as PmcpResult;
use pmcp::{
Client, ClientCapabilities, Error, PromptHandler, ResourceHandler, ServerBuilder,
ServerCapabilities, StdioTransport, ToolHandler,
};
use serde_json::{json, Value};
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::process::Command as TokioCommand;
#[derive(Clone)]
struct TestToolHandler;
#[async_trait]
impl ToolHandler for TestToolHandler {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> PmcpResult<Value> {
match args.get("operation").and_then(|v| v.as_str()) {
Some("add") => {
let a = args.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
let b = args.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(json!({ "result": a + b }))
},
Some("echo") => {
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
Ok(json!({ "message": message }))
},
_ => Err(Error::invalid_params("Unknown operation")),
}
}
}
#[derive(Clone)]
struct TestResourceHandler;
#[async_trait]
impl ResourceHandler for TestResourceHandler {
async fn read(
&self,
uri: &str,
_extra: pmcp::RequestHandlerExtra,
) -> PmcpResult<pmcp::types::ReadResourceResult> {
if uri == "test://example.txt" {
Ok(pmcp::types::ReadResourceResult::new(vec![
pmcp::types::Content::text("Hello from Rust server!"),
]))
} else {
Err(Error::not_found(uri))
}
}
async fn list(
&self,
_cursor: Option<String>,
_extra: pmcp::RequestHandlerExtra,
) -> PmcpResult<pmcp::types::ListResourcesResult> {
Ok(pmcp::types::ListResourcesResult::new(vec![
pmcp::types::ResourceInfo::new("test://example.txt", "Example Text File")
.with_description("A test resource from Rust")
.with_mime_type("text/plain"),
]))
}
}
#[derive(Clone)]
struct TestPromptHandler;
#[async_trait]
impl PromptHandler for TestPromptHandler {
async fn handle(
&self,
args: std::collections::HashMap<String, String>,
_extra: pmcp::RequestHandlerExtra,
) -> PmcpResult<pmcp::types::GetPromptResult> {
let name = args.get("name").map_or("User", |s| s.as_str());
Ok(pmcp::types::GetPromptResult::new(
vec![pmcp::types::PromptMessage::user(
pmcp::types::Content::text(format!("Please greet {}", name)),
)],
Some(format!("Greeting for {}", name)),
))
}
}
#[tokio::test]
#[ignore = "Requires TypeScript SDK setup"]
async fn test_rust_client_typescript_server() -> Result<(), Box<dyn std::error::Error>> {
if !is_node_available() {
eprintln!("Node.js not found, skipping TypeScript interop tests");
return Ok(());
}
install_typescript_sdk()?;
let mut ts_server = start_typescript_server()?;
tokio::time::sleep(Duration::from_secs(2)).await;
let transport = StdioTransport::new();
let mut client = Client::new(transport);
let init_result = client.initialize(ClientCapabilities::default()).await?;
assert_eq!(init_result.server_info.name, "typescript-test-server");
let tools = client.list_tools(None).await?;
assert!(tools.tools.len() >= 2);
assert!(tools.tools.iter().any(|t| t.name == "add"));
assert!(tools.tools.iter().any(|t| t.name == "echo"));
let add_result = client
.call_tool("add".to_string(), json!({ "a": 5, "b": 3 }))
.await?;
if let pmcp::types::Content::Text { text } = &add_result.content[0] {
assert_eq!(text, "8");
} else {
panic!("Expected text content");
}
let echo_result = client
.call_tool("echo".to_string(), json!({ "message": "Hello from Rust!" }))
.await?;
if let pmcp::types::Content::Text { text } = &echo_result.content[0] {
assert_eq!(text, "Hello from Rust!");
} else {
panic!("Expected text content");
}
let resources = client.list_resources(None).await?;
assert!(!resources.resources.is_empty());
assert_eq!(resources.resources[0].uri, "test://example.txt");
let resource = client
.read_resource("test://example.txt".to_string())
.await?;
assert_eq!(resource.contents.len(), 1);
if let pmcp::types::Content::Text { text } = &resource.contents[0] {
assert_eq!(text, "Hello from TypeScript server!");
} else {
panic!("Expected text content");
}
let prompts = client.list_prompts(None).await?;
assert!(!prompts.prompts.is_empty());
assert_eq!(prompts.prompts[0].name, "greeting");
let prompt = client
.get_prompt(
"greeting".to_string(),
std::iter::once(("name".to_string(), "Alice".to_string())).collect(),
)
.await?;
assert_eq!(prompt.messages.len(), 1);
let _ = ts_server.kill().await;
Ok(())
}
#[tokio::test]
#[ignore = "Requires TypeScript SDK setup"]
async fn test_typescript_client_rust_server() -> Result<(), Box<dyn std::error::Error>> {
if !is_node_available() {
eprintln!("Node.js not found, skipping TypeScript interop tests");
return Ok(());
}
install_typescript_sdk()?;
let server = ServerBuilder::new()
.name("rust-test-server")
.version("1.0.0")
.capabilities(ServerCapabilities::default())
.tool("echo", TestToolHandler)
.tool("add", TestToolHandler)
.resources(TestResourceHandler)
.prompt("greeting", TestPromptHandler)
.build()?;
let server_handle = tokio::spawn(async move { server.run_stdio().await });
tokio::time::sleep(Duration::from_secs(2)).await;
let output = Command::new("npm")
.args(["test", "--", "test-client.js"])
.current_dir("tests/integration/typescript-interop")
.output()
.map_err(|e| format!("Failed to run npm test: {}", e))?;
if !output.status.success() {
eprintln!("TypeScript client test failed:");
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
panic!("TypeScript client tests failed");
}
server_handle.abort();
Ok(())
}
#[tokio::test]
async fn test_protocol_compatibility() -> Result<(), Box<dyn std::error::Error>> {
if !is_node_available() {
eprintln!("Node.js not found, skipping TypeScript interop tests");
return Ok(());
}
let versions = vec!["2024-11-05", "2025-03-26", "2025-06-18"];
for version in versions {
println!("Testing protocol version: {}", version);
}
Ok(())
}
#[tokio::test]
async fn test_error_handling_interop() -> Result<(), Box<dyn std::error::Error>> {
if !is_node_available() {
eprintln!("Node.js not found, skipping TypeScript interop tests");
return Ok(());
}
Ok(())
}
#[tokio::test]
async fn test_concurrent_operations() -> Result<(), Box<dyn std::error::Error>> {
if !is_node_available() {
eprintln!("Node.js not found, skipping TypeScript interop tests");
return Ok(());
}
Ok(())
}
fn is_node_available() -> bool {
Command::new("node")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn install_typescript_sdk() -> std::result::Result<(), Box<dyn std::error::Error>> {
let output = Command::new("npm")
.arg("install")
.current_dir("tests/integration/typescript-interop")
.output()
.map_err(|e| format!("Failed to run npm install: {}", e))?;
if !output.status.success() {
return Err(format!(
"npm install failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(())
}
fn start_typescript_server(
) -> std::result::Result<tokio::process::Child, Box<dyn std::error::Error>> {
let mut cmd = TokioCommand::new("node");
cmd.arg("test-server.js")
.current_dir("tests/integration/typescript-interop")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd.spawn()
.map_err(|e| format!("Failed to start TypeScript server: {}", e).into())
}
#[allow(dead_code)]
fn start_typescript_client() -> TokioCommand {
let mut cmd = TokioCommand::new("node");
cmd.arg("test-client.js")
.current_dir("tests/integration/typescript-interop")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}