chunk-your-tools 2.0.3

MCP tool schema decomposition and recomposition
Documentation
use chunk_your_tools::{
    PolicyContext, ToolPolicy, build_catalog_from_tools, load_catalog_index_from_dir,
    policies::ToolKind, recompose_tools_from_index,
};
use serde_json::json;

#[test]
fn build_simple_tool() {
    let tool = json!({
        "id": "mcp__test__foo",
        "server": "test",
        "tool": "mcp__test__foo",
        "summary": "A test tool",
        "full_schema": {
            "id": "mcp__test__foo",
            "name": "mcp__test__foo",
            "description": "A test tool",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "required_field": {"type": "string"},
                    "optional_field": {"type": "string", "description": "opt"}
                },
                "required": ["required_field"]
            }
        }
    });
    let index = build_catalog_from_tools(&[tool]);
    assert!(
        index
            .files
            .contains_key("schemas/decomposed/mcp__test__foo.json")
    );
    assert!(index.files.keys().any(|k| k.contains("optional_field")));
}

#[test]
fn build_from_anthropic_tools() {
    let tool = json!({
        "name": "Agent",
        "description": "Launch agents",
        "input_schema": {
            "type": "object",
            "properties": {
                "prompt": {"type": "string"},
                "model": {"type": "string", "enum": ["opus", "haiku"]}
            },
            "required": ["prompt"]
        }
    });
    let index = build_catalog_from_tools(&[tool]);
    assert!(index.files.contains_key("schemas/decomposed/Agent.json"));
    assert!(index.files.keys().any(|k| k.contains("Agent/model")));
    assert!(index.files.contains_key("schemas/decomposed/haiku.md"));
}

#[test]
fn enum_md_files_without_json_quotes() {
    let index = chunk_your_tools::build_catalog_index(&[], &[json!("Bash"), json!("auto")]);
    assert_eq!(
        index
            .files
            .get("schemas/decomposed/Bash.md")
            .map(String::as_str),
        Some("Bash"),
    );
    assert_eq!(
        index
            .files
            .get("schemas/decomposed/auto.md")
            .map(String::as_str),
        Some("auto"),
    );
}

#[test]
fn recompose_from_catalog_dir_matches_in_memory() -> Result<(), String> {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/catalog");
    if !root.join("tools.json").is_file() {
        return Ok(());
    }
    let on_disk = load_catalog_index_from_dir(&root).map_err(|e| format!("load catalog: {e}"))?;
    let in_memory = build_catalog_from_tools(&on_disk.tools);
    let survivors = chunk_your_tools::NamedSurvivors {
        tools: vec!["Agent".into()],
        properties: std::collections::HashMap::new(),
        enums: vec![],
    };
    let ctx = PolicyContext::new();
    let from_disk = recompose_tools_from_index(&on_disk, &survivors, &ctx);
    let from_memory = recompose_tools_from_index(&in_memory, &survivors, &ctx);
    assert_eq!(from_disk.len(), from_memory.len());
    Ok(())
}

#[test]
fn tool_type_override_treats_agent_as_mcp() -> Result<(), String> {
    let tool = json!({
        "name": "Agent",
        "input_schema": {
            "type": "object",
            "properties": {
                "prompt": {"type": "string"},
                "model": {"type": "string", "enum": ["opus", "haiku"]}
            },
            "required": ["prompt"]
        }
    });
    let index = build_catalog_from_tools(&[tool]);
    let survivors = chunk_your_tools::NamedSurvivors {
        tools: vec!["Agent".into()],
        properties: std::collections::HashMap::new(),
        enums: vec![],
    };
    let mut ctx = PolicyContext::new();
    ctx.mcp_policy = ToolPolicy::PruneAll;
    ctx.system_policy = ToolPolicy::AlwaysInclude;
    ctx.tool_kind_override = Some(ToolKind::Mcp);
    let result = recompose_tools_from_index(&index, &survivors, &ctx);
    assert_eq!(result.len(), 1);
    let schema = result[0]
        .get("input_schema")
        .or_else(|| result[0].get("inputSchema"))
        .and_then(|v| v.get("properties"))
        .and_then(|v| v.as_object())
        .ok_or_else(|| "missing properties".to_string())?;
    assert!(!schema.contains_key("model"));
    Ok(())
}