elicitation 0.8.0

Conversational elicitation of strongly-typed Rust values via MCP
Documentation
//! Systematic tests for tool composition.
//!
//! Goal: Understand exactly where composition breaks down.

use elicitation::Elicit;
use elicitation_macros::elicit_tools;
use rmcp::handler::server::wrapper::Json;
use rmcp::model::{ServerCapabilities, ServerInfo};
use rmcp::service::{Peer, RoleServer};
use rmcp::{ErrorData, ServerHandler, tool, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

// ============================================================================
// Test 1: Baseline - Regular tools only (should work)
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct RegularResponse {
    message: String,
}

struct RegularOnlyServer;

#[tool_router]
impl RegularOnlyServer {
    #[tool(description = "Regular tool")]
    pub async fn regular(_peer: Peer<RoleServer>) -> Result<Json<RegularResponse>, ErrorData> {
        Ok(Json(RegularResponse {
            message: "ok".to_string(),
        }))
    }
}

impl ServerHandler for RegularOnlyServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

#[test]
fn test_1_regular_only_baseline() {
    let router = RegularOnlyServer::tool_router();
    let tools = router.list_all();

    println!("Test 1 - Regular only: {} tools", tools.len());
    for tool in &tools {
        println!("  - {}", tool.name);
    }

    assert_eq!(tools.len(), 1, "Should have 1 regular tool");
}

// ============================================================================
// Test 2: Elicit tools only (we know this works)
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Elicit)]
struct TestData {
    value: String,
}

struct ElicitOnlyServer;

#[elicit_tools(TestData)]
#[tool_router]
impl ElicitOnlyServer {}

impl ServerHandler for ElicitOnlyServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

#[test]
fn test_2_elicit_only_works() {
    let router = ElicitOnlyServer::tool_router();
    let tools = router.list_all();

    println!("Test 2 - Elicit only: {} tools", tools.len());
    for tool in &tools {
        println!("  - {}", tool.name);
    }

    assert_eq!(tools.len(), 1, "Should have 1 elicit tool");
}

// ============================================================================
// Test 3: Check if _tool_attr functions are generated
// ============================================================================

#[test]
fn test_3_tool_attr_functions_exist() {
    // These functions should be generated by #[elicit_tools]
    let data_tool = ElicitOnlyServer::elicit_test_data_tool_attr();

    println!("Test 3 - Tool attr functions:");
    println!("  - {}", data_tool.name);

    assert_eq!(data_tool.name, "elicit_test_data");
}

// ============================================================================
// Test 4: ONE regular tool + elicit tools (the critical test)
// ============================================================================

struct ComposedServer;

#[elicit_tools(TestData)]
#[tool_router]
impl ComposedServer {
    #[tool(description = "Regular tool")]
    pub async fn regular(_peer: Peer<RoleServer>) -> Result<Json<RegularResponse>, ErrorData> {
        Ok(Json(RegularResponse {
            message: "ok".to_string(),
        }))
    }
}

impl ServerHandler for ComposedServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

#[test]
fn test_4_composition() {
    let router = ComposedServer::tool_router();
    let tools = router.list_all();

    println!("Test 4 - Composed: {} tools", tools.len());
    for tool in &tools {
        println!("  - {}", tool.name);
    }

    assert_eq!(tools.len(), 2, "Should have 1 regular + 1 elicit = 2 tools");
}

// ============================================================================
// Test 5: Multiple tools of each kind
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Elicit)]
struct Config {
    name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Elicit)]
struct User {
    username: String,
}

struct MultiToolServer;

#[elicit_tools(Config, User, TestData)]
#[tool_router]
impl MultiToolServer {
    #[tool(description = "Get status")]
    pub async fn status(_peer: Peer<RoleServer>) -> Result<Json<RegularResponse>, ErrorData> {
        Ok(Json(RegularResponse {
            message: "running".to_string(),
        }))
    }

    #[tool(description = "Get version")]
    pub async fn version(_peer: Peer<RoleServer>) -> Result<Json<RegularResponse>, ErrorData> {
        Ok(Json(RegularResponse {
            message: "1.0.0".to_string(),
        }))
    }
}

impl ServerHandler for MultiToolServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

#[test]
fn test_5_multiple_tools_each_kind() {
    let router = MultiToolServer::tool_router();
    let tools = router.list_all();

    println!("Test 5 - Multiple of each: {} tools", tools.len());
    for tool in &tools {
        println!("  - {}", tool.name);
    }

    // Should have 2 regular + 3 elicit = 5 total
    assert_eq!(tools.len(), 5, "Should have 2 regular + 3 elicit = 5 tools");

    let names: Vec<String> = tools.iter().map(|t| t.name.to_string()).collect();
    assert!(names.contains(&"status".to_string()));
    assert!(names.contains(&"version".to_string()));
    assert!(names.contains(&"elicit_config".to_string()));
    assert!(names.contains(&"elicit_user".to_string()));
    assert!(names.contains(&"elicit_test_data".to_string()));
}