pmcp 2.2.0

High-quality Rust SDK for Model Context Protocol (MCP) with full TypeScript SDK compatibility
Documentation
//! Integration tests for TypeScript SDK interoperability.
//!
//! These tests ensure that PMCP Rust SDK can communicate correctly with
//! the official TypeScript SDK implementation.

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;

/// Test tool handler for integration tests
#[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")),
        }
    }
}

/// Test resource handler
#[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"),
        ]))
    }
}

/// Test prompt handler
#[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>> {
    // Skip if Node.js is not available
    if !is_node_available() {
        eprintln!("Node.js not found, skipping TypeScript interop tests");
        return Ok(());
    }

    // Install TypeScript SDK if needed
    install_typescript_sdk()?;

    // Start TypeScript server
    let mut ts_server = start_typescript_server()?;

    // Give server time to start
    tokio::time::sleep(Duration::from_secs(2)).await;

    // Create Rust client
    let transport = StdioTransport::new();
    let mut client = Client::new(transport);

    // Initialize connection
    let init_result = client.initialize(ClientCapabilities::default()).await?;
    assert_eq!(init_result.server_info.name, "typescript-test-server");

    // Test tool listing
    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"));

    // Test tool calling
    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");
    }

    // Test resource listing
    let resources = client.list_resources(None).await?;
    assert!(!resources.resources.is_empty());
    assert_eq!(resources.resources[0].uri, "test://example.txt");

    // Test resource reading
    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");
    }

    // Test prompt listing
    let prompts = client.list_prompts(None).await?;
    assert!(!prompts.prompts.is_empty());
    assert_eq!(prompts.prompts[0].name, "greeting");

    // Test prompt getting
    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);

    // Clean up
    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>> {
    // Skip if Node.js is not available
    if !is_node_available() {
        eprintln!("Node.js not found, skipping TypeScript interop tests");
        return Ok(());
    }

    // Install TypeScript SDK if needed
    install_typescript_sdk()?;

    // Create and start Rust server
    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()?;

    // Run server in background
    let server_handle = tokio::spawn(async move { server.run_stdio().await });

    // Give server time to start
    tokio::time::sleep(Duration::from_secs(2)).await;

    // Run TypeScript client tests
    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");
    }

    // Clean up
    server_handle.abort();

    Ok(())
}

#[tokio::test]
async fn test_protocol_compatibility() -> Result<(), Box<dyn std::error::Error>> {
    // Skip if Node.js is not available
    if !is_node_available() {
        eprintln!("Node.js not found, skipping TypeScript interop tests");
        return Ok(());
    }

    // Test protocol version negotiation
    let versions = vec!["2024-11-05", "2025-03-26", "2025-06-18"];

    for version in versions {
        println!("Testing protocol version: {}", version);

        // Protocol version testing would require server-side support
        // Currently testing with default protocol version
    }

    Ok(())
}

#[tokio::test]
async fn test_error_handling_interop() -> Result<(), Box<dyn std::error::Error>> {
    // Skip if Node.js is not available
    if !is_node_available() {
        eprintln!("Node.js not found, skipping TypeScript interop tests");
        return Ok(());
    }

    // Test that errors are properly propagated between implementations

    // Error handling tests require setting up specific error scenarios
    // in both TypeScript and Rust servers - deferred to integration tests

    Ok(())
}

#[tokio::test]
async fn test_concurrent_operations() -> Result<(), Box<dyn std::error::Error>> {
    // Skip if Node.js is not available
    if !is_node_available() {
        eprintln!("Node.js not found, skipping TypeScript interop tests");
        return Ok(());
    }

    // Test multiple concurrent operations between Rust and TypeScript

    // Concurrent operation tests implemented in stress testing suite

    Ok(())
}

// Helper functions

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
}