tools-kit 0.0.2

Portable Local and MCP tool execution with narrow extension traits.
Documentation
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use serde_json::json;
use tools_kit::{
    ToolError, ToolHandler, ToolHandlerBackend, Toolset, ToolsetBackend,
    declaration::{ToolAnnotations, ToolIcon, ToolInfo, ToolName},
    execution::{ToolContent, ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult},
    interaction::{
        ToolInteractionCapabilities, ToolInteractionId, ToolInteractionKind,
        ToolInteractionRequest, ToolInteractionResponse,
    },
    local::{ToolInvocationContext, ToolRegistration},
    mcp::{McpClientConfig, McpConnectionConfig},
    schema::{JsonSchema, SchemaStrictness, ValidationIssue},
};

#[derive(Deserialize, schemars::JsonSchema)]
struct EchoArgs {
    message: String,
}

#[derive(Serialize, schemars::JsonSchema)]
struct EchoOutput {
    echoed: String,
}

async fn macro_echo(args: EchoArgs) -> Result<EchoOutput, ToolError> {
    Ok(EchoOutput {
        echoed: args.message,
    })
}

#[test]
fn primary_handles_and_functional_module_types_have_expected_auto_traits() {
    fn assert_send<T: Send>() {}
    fn assert_send_sync<T: Send + Sync>() {}
    fn assert_clone<T: Clone>() {}
    fn assert_handler_backend<T: ToolHandlerBackend>() {}
    fn assert_toolset_backend<T: ToolsetBackend>() {}

    assert_clone::<Toolset>();
    assert_send_sync::<Toolset>();
    assert_toolset_backend::<Toolset>();
    assert_send::<ToolHandler>();
    assert_handler_backend::<ToolHandler>();

    assert_send_sync::<ToolRegistration>();
    assert_send_sync::<ToolInvocationContext>();
    assert_send_sync::<McpConnectionConfig>();
    assert_send_sync::<McpClientConfig>();
    assert_send_sync::<ToolName>();
    assert_send_sync::<ToolInfo>();
    assert_send_sync::<ToolAnnotations>();
    assert_send_sync::<ToolIcon>();
    assert_send_sync::<ToolInvocation>();
    assert_send_sync::<ToolResult>();
    assert_send_sync::<ToolContent>();
    assert_send_sync::<ToolHandlerInput>();
    assert_send_sync::<ToolHandlerOutput>();
    assert_send_sync::<ToolInteractionCapabilities>();
    assert_send_sync::<ToolInteractionRequest>();
    assert_send_sync::<ToolInteractionResponse>();
    assert_send_sync::<ToolInteractionId>();
    assert_send_sync::<ToolInteractionKind>();
    assert_send_sync::<JsonSchema>();
    assert_send_sync::<SchemaStrictness>();
    assert_send_sync::<ValidationIssue>();
    assert_send_sync::<ToolError>();

    #[cfg(feature = "mcp-server")]
    assert_send::<tools_kit::mcp::McpServer>();
}

#[test]
fn mcp_configuration_debug_redacts_transport_secrets() {
    let stdio = McpClientConfig::new(McpConnectionConfig::Stdio {
        program: "fixture-mcp-server".to_owned(),
        args: vec![
            "--access-token".to_owned(),
            "STDIO_ARGUMENT_SECRET".to_owned(),
        ],
        cwd: Some("/fixture/WORKING_DIRECTORY_SECRET".into()),
        env: BTreeMap::from([(
            "FIXTURE_TOKEN".to_owned(),
            "STDIO_ENVIRONMENT_SECRET".to_owned(),
        )]),
    });
    let rendered = format!("{stdio:?}");
    assert!(rendered.contains("fixture-mcp-server"));
    assert!(rendered.contains("argument_count: 2"));
    assert!(rendered.contains("has_working_directory: true"));
    assert!(rendered.contains("environment_count: 1"));
    for secret in [
        "--access-token",
        "STDIO_ARGUMENT_SECRET",
        "FIXTURE_TOKEN",
        "STDIO_ENVIRONMENT_SECRET",
        "WORKING_DIRECTORY_SECRET",
    ] {
        assert!(!rendered.contains(secret));
    }

    let http = McpClientConfig::new(McpConnectionConfig::StreamableHttp {
        endpoint: "https://URL_USER_SECRET:URL_PASSWORD_SECRET@example.invalid:8443/PATH_SECRET?access_token=URL_QUERY_SECRET#URL_FRAGMENT_SECRET"
            .parse()
            .expect("fixture MCP endpoint should parse"),
        headers: BTreeMap::from([(
            "Authorization".to_owned(),
            "Bearer HTTP_HEADER_SECRET".to_owned(),
        )]),
    });
    let rendered = format!("{http:?}");
    assert!(rendered.contains("endpoint_scheme: \"https\""));
    assert!(rendered.contains("endpoint_host: Some(\"example.invalid\")"));
    assert!(rendered.contains("endpoint_port: Some(8443)"));
    assert!(rendered.contains("header_count: 1"));
    for secret in [
        "URL_USER_SECRET",
        "URL_PASSWORD_SECRET",
        "PATH_SECRET",
        "URL_QUERY_SECRET",
        "URL_FRAGMENT_SECRET",
        "Authorization",
        "HTTP_HEADER_SECRET",
    ] {
        assert!(!rendered.contains(secret));
    }
}

#[test]
fn tool_name_is_opaque_and_serializes_as_rendered_name() {
    let name = ToolName::new("demo_echo").expect("valid tool name should parse");
    assert_eq!(name.as_str(), "demo_echo");
    assert_eq!(serde_json::to_value(&name).unwrap(), json!("demo_echo"));
    let decoded: ToolName = serde_json::from_value(json!("demo_echo")).unwrap();
    assert_eq!(decoded, name);
}

#[tokio::test]
async fn macro_and_closure_registrations_execute_through_the_public_hierarchy() {
    let suffix = " captured".to_owned();
    let toolset = Toolset::local(vec![
        tools_kit::tool!(
            "macro_echo",
            description = "Echo via a function item.",
            macro_echo
        )
        .expect("function registration should validate"),
        tools_kit::tool!("macro_closure", move |args: EchoArgs| {
            let suffix = suffix.clone();
            async move {
                Ok(EchoOutput {
                    echoed: format!("{}{}", args.message, suffix),
                })
            }
        })
        .expect("closure registration should validate"),
    ])
    .expect("local Toolset should build");

    let tools = toolset.list_tools().await.expect("inventory should load");
    let macro_echo = tools
        .iter()
        .find(|tool| tool.name.as_str() == "macro_echo")
        .expect("function registration should be listed");
    assert_eq!(
        macro_echo.description.as_deref(),
        Some("Echo via a function item.")
    );

    let mut handler = toolset
        .tool_handler(ToolName::new("macro_closure").unwrap())
        .await
        .expect("handler should build");
    let output = handler
        .step(ToolHandlerInput::Invoke(ToolInvocation {
            call_id: Some("call-1".to_owned()),
            arguments: json!({ "message": "hello" }),
        }))
        .await
        .expect("closure should execute");
    let ToolHandlerOutput::Completed(result) = output else {
        panic!("closure should complete without interaction");
    };
    assert_eq!(
        result.structured_content,
        Some(json!({ "echoed": "hello captured" }))
    );
}

#[test]
fn interaction_response_exposes_only_source_neutral_identity() {
    let id = ToolInteractionId::new();
    let response = ToolInteractionResponse::submit(id.clone(), json!({ "ok": true }));
    assert_eq!(response.interaction_id(), &id);
}