memcan 1.1.0

Persistent memory for Claude Code — CLI client
//! MCP HTTP client wrapper for thin CLI operations.

use std::path::Path;

use rmcp::{
    RoleClient, ServiceExt, model::CallToolRequestParams, service::RunningService,
    transport::StreamableHttpClientTransport,
    transport::streamable_http_client::StreamableHttpClientTransportConfig,
};

use crate::CliConfig;

pub struct McpClient {
    service: RunningService<RoleClient, ()>,
}

impl McpClient {
    pub async fn connect(config: &CliConfig) -> Result<Self, Box<dyn std::error::Error>> {
        let mut url = config.url.clone();
        if !url.ends_with("/mcp") {
            url = format!("{}/mcp", url.trim_end_matches('/'));
        }

        let mut transport_config = StreamableHttpClientTransportConfig::with_uri(url);
        if let Some(ref key) = config.api_key {
            transport_config = transport_config.auth_header(key.clone());
        }

        let transport =
            StreamableHttpClientTransport::with_client(reqwest::Client::new(), transport_config);
        let client = ().serve(transport).await?;

        Ok(Self { service: client })
    }

    pub async fn call_tool(
        &self,
        name: &str,
        arguments: serde_json::Value,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let json_obj: rmcp::model::JsonObject = arguments
            .as_object()
            .cloned()
            .unwrap_or_default()
            .into_iter()
            .collect();

        let params = CallToolRequestParams::new(name.to_string()).with_arguments(json_obj);

        let result = self.service.call_tool(params).await?;

        let texts: Vec<String> = result
            .content
            .iter()
            .filter_map(|c| c.as_text().map(|t| t.text.to_string()))
            .collect();

        Ok(texts.join("\n"))
    }

    pub async fn close(self) {
        drop(self.service);
    }
}

pub fn load_config() -> CliConfig {
    let config_path = dirs::config_dir()
        .map(|d| d.join("memcan").join(".env"))
        .unwrap_or_default();

    let cwd_env = Path::new(".env");
    if cwd_env.exists() {
        let _ = dotenvy::from_path(cwd_env);
    }

    if config_path.exists() {
        let _ = dotenvy::from_path(&config_path);
    }

    CliConfig {
        url: std::env::var("MEMCAN_URL").unwrap_or_else(|_| "http://localhost:8190".to_string()),
        api_key: std::env::var("MEMCAN_API_KEY")
            .ok()
            .filter(|s| !s.is_empty()),
    }
}