reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! MCP Daemon Module
//!
//! Provides optional background daemon for persistent MCP server connections.
//!
//! ## Architecture
//!
//! - **Direct Mode**: Spawn MCP clients on-demand (default, no daemon required)
//! - **Daemon Mode**: Persistent background process with connection pooling
//!
//! ## Usage
//!
//! ```bash
//! # Start daemon
//! rk mcp daemon start
//!
//! # Call tool (auto-detects daemon)
//! rk mcp call-tool gigathink '{"query": "What is reasoning?"}'
//!
//! # Stop daemon
//! rk mcp daemon stop
//! ```
//!
//! ## Configuration
//!
//! Custom servers can be configured in `~/.config/reasonkit/mcp_servers.json`:
//!
//! ```json
//! [
//!   {
//!     "name": "custom-server",
//!     "command": "/path/to/server",
//!     "args": ["--port", "3000"],
//!     "tools": ["custom_tool"]
//!   }
//! ]
//! ```

pub mod config;
pub mod health;
pub mod ipc_client;
pub mod ipc_server;
pub mod logger;
pub mod manager;
pub mod pool;
pub mod signals;

pub use config::{ServerConfigEntry, ServerRegistry};
pub use health::{DaemonHealth, HealthMonitor, HealthStatus};
pub use ipc_client::IpcClient;
pub use ipc_server::{DaemonServer, IpcMessage, ToolInfo};
pub use logger::DaemonLogger;
pub use manager::{DaemonManager, DaemonStatus};
pub use pool::{ConnectionPool, PoolStats};
pub use signals::setup_signal_handlers;

use crate::error::{Error, Result};
use crate::mcp::tools::ToolResult;
use std::sync::OnceLock;
use tokio::sync::RwLock;

/// Global server registry (lazy-loaded)
static SERVER_REGISTRY: OnceLock<RwLock<ServerRegistry>> = OnceLock::new();

/// Get or initialize the server registry
fn get_registry() -> &'static RwLock<ServerRegistry> {
    SERVER_REGISTRY.get_or_init(|| {
        RwLock::new(ServerRegistry::load().unwrap_or_else(|e| {
            tracing::warn!(error = %e, "Failed to load server registry, using defaults");
            ServerRegistry::new()
        }))
    })
}

/// Global connection pool (lazy-loaded)
static CONNECTION_POOL: OnceLock<ConnectionPool> = OnceLock::new();

/// Get or initialize the connection pool
fn get_pool() -> &'static ConnectionPool {
    CONNECTION_POOL.get_or_init(ConnectionPool::new)
}

/// Check if daemon is running
pub async fn daemon_is_running() -> Result<bool> {
    let manager = DaemonManager::new()?;
    Ok(matches!(
        manager.status().await,
        DaemonStatus::Running { .. }
    ))
}

/// Call tool via daemon (if running) or direct mode with pooling
pub async fn call_tool(name: &str, args: serde_json::Value) -> Result<ToolResult> {
    if daemon_is_running().await? {
        // Use daemon via IPC (fastest)
        daemon_call_tool(name, args).await
    } else {
        // Direct execution with connection pooling
        pooled_call_tool(name, args).await
    }
}

/// Call tool via daemon IPC
async fn daemon_call_tool(name: &str, args: serde_json::Value) -> Result<ToolResult> {
    let mut client = IpcClient::connect().await?;
    client.call_tool(name, args).await
}

/// Call tool with connection pooling (no daemon)
async fn pooled_call_tool(name: &str, args: serde_json::Value) -> Result<ToolResult> {
    let pool = get_pool();
    let registry = get_registry().read().await;

    // Get server config for this tool
    let config = registry
        .get_client_config(name)
        .ok_or_else(|| Error::network(format!("Unknown tool '{}' - no server configured", name)))?;

    // Get or create pooled connection
    pool.call_tool(&config, name, args).await
}

/// Force direct execution (bypasses daemon and pool)
pub async fn direct_call_tool(name: &str, args: serde_json::Value) -> Result<ToolResult> {
    use crate::mcp::{McpClient, McpClientTrait};

    let registry = get_registry().read().await;
    let config = registry
        .get_client_config(name)
        .ok_or_else(|| Error::network(format!("Unknown tool '{}' - no server configured", name)))?;

    // Create temporary client (no pooling)
    let mut client = McpClient::new(config);
    client.connect().await?;
    let result = client.call_tool(name, args).await?;
    client.disconnect().await?;

    Ok(result)
}

/// List all available tools from registry
pub async fn list_available_tools() -> Vec<String> {
    let registry = get_registry().read().await;
    registry
        .list_tools()
        .iter()
        .map(|s| s.to_string())
        .collect()
}

/// List all registered servers
pub async fn list_servers() -> Vec<String> {
    let registry = get_registry().read().await;
    registry
        .list_servers()
        .iter()
        .map(|s| s.to_string())
        .collect()
}

/// Reload server registry from config file
pub async fn reload_config() -> Result<()> {
    let mut registry = get_registry().write().await;
    *registry = ServerRegistry::load()?;
    tracing::info!("Server registry reloaded");
    Ok(())
}

/// Clear connection pool (force reconnection on next call)
pub fn clear_pool() {
    get_pool().clear();
    tracing::info!("Connection pool cleared");
}

/// Get connection pool statistics
pub fn pool_stats() -> pool::PoolStats {
    get_pool().stats()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_daemon_detection() {
        // Should not be running initially
        let running = daemon_is_running().await.unwrap();
        assert!(!running);
    }

    #[tokio::test]
    async fn test_registry_loading() {
        let registry = get_registry().read().await;

        // Built-in tools should be available
        assert!(registry.get_server_for_tool("gigathink").is_some());
        assert!(registry.get_server_for_tool("think").is_some());
    }

    #[tokio::test]
    async fn test_list_available_tools() {
        let tools = list_available_tools().await;

        assert!(tools.contains(&"gigathink".to_string()));
        assert!(tools.contains(&"laserlogic".to_string()));
        assert!(tools.contains(&"think".to_string()));
    }

    #[tokio::test]
    async fn test_unknown_tool_error() {
        let result = direct_call_tool("nonexistent_tool", serde_json::json!({})).await;
        assert!(result.is_err());

        let error = result.unwrap_err().to_string();
        assert!(error.contains("Unknown tool"));
    }

    #[test]
    fn test_pool_stats() {
        let stats = pool_stats();
        assert_eq!(stats.active_connections, 0);
        assert_eq!(stats.total_calls, 0);
    }
}