cloudiful-server 0.2.2

Rust web server bootstrap crate with Actix and Axum adapters
Documentation
use rmcp::ServiceExt;

use crate::{
    ServerConfig as HttpServerConfig,
    mcp::{Server, serve, tool, tool_router},
};

#[derive(Clone)]
struct PingServer;

#[tool_router(server_handler)]
impl PingServer {
    #[tool(description = "Ping the server")]
    fn ping(&self) -> String {
        "pong".to_string()
    }
}

#[tokio::test]
async fn serve_over_async_rw_transport_exposes_tools() {
    let (server_io, client_io) = tokio::io::duplex(1024);
    let (server_read, server_write) = tokio::io::split(server_io);
    let (client_read, client_write) = tokio::io::split(client_io);

    let server_task =
        tokio::spawn(async move { serve(PingServer, (server_read, server_write)).await });

    let client = ().serve((client_read, client_write)).await.unwrap();
    let tools = client.peer().list_tools(Default::default()).await.unwrap();

    assert_eq!(tools.tools.len(), 1);
    assert_eq!(tools.tools[0].name, "ping");

    client.cancel().await.unwrap();
    server_task.await.unwrap().unwrap().cancel().await.unwrap();
}

#[tokio::test]
async fn streamable_http_server_binds_on_loopback() {
    let config = HttpServerConfig::new()
        .with_listen_addr("127.0.0.1:0")
        .build()
        .unwrap();

    let bound = Server::new(config, || PingServer)
        .with_server_config(crate::mcp::ServerConfig::new().with_service_path("/mcp"))
        .bind()
        .unwrap();

    assert!(bound.addrs().iter().all(|addr| addr.ip().is_loopback()));
}

#[tokio::test]
async fn invalid_service_path_is_rejected() {
    let config = HttpServerConfig::new()
        .with_listen_addr("127.0.0.1:0")
        .build()
        .unwrap();

    let err = Server::new(config, || PingServer)
        .with_server_config(crate::mcp::ServerConfig::new().with_service_path("mcp"))
        .bind()
        .unwrap_err();

    assert!(matches!(
        err,
        crate::mcp::McpServerError::InvalidServicePath(_)
    ));
}