tools-kit 0.0.2

Portable Local and MCP tool execution with narrow extension traits.
Documentation
pub mod support;

use std::collections::BTreeMap;

use serde_json::json;
use support::mcp_stdio_harness;
use tools_kit::{
    ToolError, Toolset, ToolsetBackend,
    mcp::{McpClientConfig, McpConnectionConfig},
};

#[tokio::test]
async fn stdio_mcp_lists_and_executes_remote_tools() {
    let toolset = mcp_stdio_harness::runtime(Some("repo")).await;
    let tools = toolset.list_tools().await.expect("inventory should load");
    assert!(tools.iter().any(|tool| tool.name.as_str() == "repo__echo"));

    let result = support::invoke_tool(
        &toolset,
        "repo__echo",
        Some("call-1"),
        json!({ "message": "hello" }),
    )
    .await
    .expect("stdio MCP echo should execute");

    assert_eq!(
        result.structured_content,
        Some(json!({ "echoed": "hello" }))
    );
}

#[tokio::test]
async fn stdio_mcp_validates_arguments_and_preserves_model_visible_errors() {
    let toolset = mcp_stdio_harness::runtime(Some("repo")).await;
    let error = support::invoke_tool(
        &toolset,
        "repo__echo",
        Some("call-invalid"),
        json!({ "message": 42 }),
    )
    .await
    .expect_err("invalid MCP arguments should fail");
    assert!(matches!(
        error,
        ToolError::InvalidArguments { .. } | ToolError::Protocol { .. }
    ));

    let result = support::invoke_tool(
        &toolset,
        "repo__error_metadata",
        Some("call-error"),
        json!({}),
    )
    .await
    .expect("model-visible failure remains a completed ToolResult");
    assert!(result.is_error);
    assert_eq!(
        result.metadata.get("trace_id"),
        Some(&json!("mcp-error-result"))
    );
}

#[tokio::test]
async fn stdio_mcp_preserves_content_metadata() {
    let toolset = mcp_stdio_harness::runtime(Some("repo")).await;
    let result = support::invoke_tool(
        &toolset,
        "repo__content_metadata",
        Some("call-content"),
        json!({}),
    )
    .await
    .expect("content metadata tool should execute");

    assert_eq!(
        result.content[0],
        tools_kit::execution::ToolContent::Text {
            text: "content metadata".to_owned(),
            metadata: Some(json!({
                "content_trace": "mcp-content",
                "priority": 0.75
            })),
        }
    );
}

#[tokio::test]
async fn stdio_mcp_shutdown_closes_the_owned_server_session() {
    let marker = tempfile::NamedTempFile::new().unwrap().into_temp_path();
    let config = mcp_stdio_harness::stdio_source(Some("repo"), Some(&marker), 2);
    let toolset = Toolset::mcp(config)
        .await
        .expect("stdio MCP toolset should build");

    toolset.shutdown().await.expect("shutdown should close MCP");
    assert_eq!(
        std::fs::read(&marker).expect("exit marker should be written"),
        b"closed"
    );
}

#[test]
fn mcp_client_rejects_an_empty_namespace() {
    let error = McpClientConfig::new(McpConnectionConfig::Stdio {
        program: "unused".to_owned(),
        args: Vec::new(),
        cwd: None,
        env: BTreeMap::new(),
    })
    .with_namespace("")
    .expect_err("empty namespace should fail");

    assert!(matches!(error, ToolError::InvalidConfiguration { .. }));
}