terraphim_mcp_server 1.0.0

Model Context Protocol (MCP) server for Terraphim AI
Documentation
mod support;

use anyhow::Result;
use rmcp::{model::CallToolRequestParam, service::ServiceExt, transport::TokioChildProcess};
use serde_json::json;
use std::process::Stdio;
use tokio::process::Command;

/// Test that MCP server properly separates logs from JSON-RPC responses
#[tokio::test]
async fn test_mcp_log_separation_and_tools() -> Result<()> {
    println!("๐Ÿงช Testing MCP server log separation and tool availability");

    // Build the server first
    let mut build = Command::new("cargo");
    build
        .arg("build")
        .arg("--package")
        .arg("terraphim_mcp_server");

    if std::env::var_os("CI").is_some() {
        build.arg("--features").arg("zlob");
    }

    let build_status = build.status().await?;

    if !build_status.success() {
        anyhow::bail!("Failed to build terraphim_mcp_server");
    }

    let mut cmd = Command::new(support::mcp_server_binary()?);
    cmd.stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .env("RUST_LOG", "info");

    // Create transport and connect
    let transport = TokioChildProcess::new(cmd)?;
    let service = ().serve(transport).await?;

    println!("๐Ÿ”— Connected to MCP server: {:?}", service.peer_info());

    // Test 1: List tools to verify server is working
    println!("๐Ÿ“‹ Testing tools/list...");
    let tools = service.list_tools(Default::default()).await?;
    println!("โœ… Found {} tools", tools.tools.len());

    // Verify we have expected tools
    let expected_tools = vec![
        "search",
        "autocomplete_terms",
        "find_matches",
        "load_thesaurus",
        "update_config_tool",
    ];

    for expected_tool in &expected_tools {
        let tool_found = tools.tools.iter().any(|t| t.name == *expected_tool);
        if tool_found {
            println!("  โœ… Tool '{}' found", expected_tool);
        } else {
            println!("  โš ๏ธ Tool '{}' not found", expected_tool);
        }
    }

    assert!(
        !tools.tools.is_empty(),
        "Should have at least some tools available"
    );

    // Test 2: Test autocomplete functionality
    println!("๐Ÿ”ค Testing autocomplete...");
    let autocomplete_result = service
        .call_tool(CallToolRequestParam {
            name: "autocomplete_terms".into(),
            arguments: json!({
                "query": "terra",
                "limit": 5
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!("โœ… Autocomplete result: {:?}", autocomplete_result.content);

    // Test 3: Test search functionality (should work without errors even if no results)
    println!("๐Ÿ” Testing search...");
    let search_result = service
        .call_tool(CallToolRequestParam {
            name: "search".into(),
            arguments: json!({
                "query": "test",
                "limit": 5
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!("โœ… Search completed: {:?}", search_result.content);

    // Test 4: Test thesaurus loading
    println!("๐Ÿ“š Testing thesaurus loading...");
    let thesaurus_result = service
        .call_tool(CallToolRequestParam {
            name: "load_thesaurus".into(),
            arguments: json!({
                "role_name": "Default",
                // Use local KG that exists in this repo; avoids needing role config automata_path
                "automata_path": "docs/src/kg"
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!(
        "โœ… Thesaurus loading result: {:?}",
        thesaurus_result.content
    );

    println!("๐ŸŽ‰ All MCP tests passed successfully!");
    Ok(())
}

/// Test MCP server with role switching and configuration updates
#[tokio::test]
async fn test_mcp_role_configuration() -> Result<()> {
    println!("โš™๏ธ Testing MCP role configuration");

    let mut cmd = Command::new(support::mcp_server_binary()?);
    cmd.stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let transport = TokioChildProcess::new(cmd)?;
    let service = ().serve(transport).await?;

    // Test configuration update
    println!("๐Ÿ”„ Testing configuration update...");
    let test_config = json!({
        "roles": {
            "Test Role": {
                "name": "Test Role",
                "shortname": "test",
                "relevance_function": "TitleScorer",
                "theme": "lumen",
                "haystacks": [],
                "kg": null,
                "terraphim_it": false,
                "extra": {}
            }
        },
        "selected_role": "Test Role",
        "default_role": "Test Role",
        "global_shortcut": "Ctrl+Space"
    });

    let config_result = service
        .call_tool(CallToolRequestParam {
            name: "update_config_tool".into(),
            arguments: json!({
                "config_str": test_config.to_string()
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!(
        "โœ… Configuration update result: {:?}",
        config_result.content
    );

    // Test search with updated config
    let search_result = service
        .call_tool(CallToolRequestParam {
            name: "search".into(),
            arguments: json!({
                "query": "test",
                "role": "Test Role",
                "limit": 3
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!(
        "โœ… Search with role configuration: {:?}",
        search_result.content
    );

    println!("๐ŸŽ‰ MCP role configuration test passed!");
    Ok(())
}

/// Test text processing tools (find_matches, replace_matches, etc.)
#[tokio::test]
async fn test_mcp_text_processing_tools() -> Result<()> {
    println!("๐Ÿ“ Testing MCP text processing tools");

    let mut cmd = Command::new(support::mcp_server_binary()?);
    cmd.stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let transport = TokioChildProcess::new(cmd)?;
    let service = ().serve(transport).await?;

    // Test find_matches
    println!("๐Ÿ” Testing find_matches...");
    let find_result = service
        .call_tool(CallToolRequestParam {
            name: "find_matches".into(),
            arguments: json!({
                "text": "This is a test document with some test words",
                "patterns": ["test", "document"]
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!("โœ… find_matches result: {:?}", find_result.content);

    // Test replace_matches
    println!("๐Ÿ”„ Testing replace_matches...");
    let replace_result = service
        .call_tool(CallToolRequestParam {
            name: "replace_matches".into(),
            arguments: json!({
                "text": "This is a test document",
                "patterns": ["test"],
                "replacement": "sample",
                // required by tool schema
                "link_type": "PlainText"
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!("โœ… replace_matches result: {:?}", replace_result.content);

    // Test extract_paragraphs_from_automata
    println!("๐Ÿ“„ Testing extract_paragraphs_from_automata...");
    let extract_result = service
        .call_tool(CallToolRequestParam {
            name: "extract_paragraphs_from_automata".into(),
            arguments: json!({
                "text": "First paragraph.\n\nSecond paragraph with test content.\n\nThird paragraph.",
                "terms": ["test"]
            })
            .as_object()
            .cloned(),
        })
        .await?;

    println!("โœ… extract_paragraphs result: {:?}", extract_result.content);

    println!("๐ŸŽ‰ All text processing tools working correctly!");
    Ok(())
}