redisctl-mcp 0.8.3

MCP (Model Context Protocol) server for Redis Cloud and Enterprise
Documentation
//! Direct Redis database tools

mod diagnostics;
mod keys;
mod raw;
mod server;
mod structures;

#[allow(unused_imports)]
pub use diagnostics::*;
#[allow(unused_imports)]
pub use keys::*;
#[allow(unused_imports)]
pub use raw::*;
#[allow(unused_imports)]
pub use server::*;
#[allow(unused_imports)]
pub use structures::*;

use std::sync::Arc;

use tower_mcp::{McpRouter, ToolError};

use super::SubModule;
use crate::state::AppState;

/// Sub-modules within the Database (Redis) toolset, each with its own tool names and router.
pub const SUB_MODULES: &[SubModule] = &[
    SubModule {
        name: "server",
        tool_names: server::TOOL_NAMES,
    },
    SubModule {
        name: "keys",
        tool_names: keys::TOOL_NAMES,
    },
    SubModule {
        name: "structures",
        tool_names: structures::TOOL_NAMES,
    },
    SubModule {
        name: "diagnostics",
        tool_names: diagnostics::TOOL_NAMES,
    },
    SubModule {
        name: "raw",
        tool_names: raw::TOOL_NAMES,
    },
];

/// Get all Database tool names as owned strings.
pub fn tool_names() -> Vec<String> {
    SUB_MODULES
        .iter()
        .flat_map(|sm| sm.tool_names.iter().map(|s| (*s).to_string()))
        .collect()
}

/// Get tool names for a specific sub-module by name.
pub fn sub_tool_names(name: &str) -> Option<&'static [&'static str]> {
    SUB_MODULES
        .iter()
        .find(|sm| sm.name == name)
        .map(|sm| sm.tool_names)
}

/// Build an MCP sub-router for a specific sub-module by name.
pub fn sub_router(name: &str, state: Arc<AppState>) -> Option<McpRouter> {
    match name {
        "server" => Some(server::router(state)),
        "keys" => Some(keys::router(state)),
        "structures" => Some(structures::router(state)),
        "diagnostics" => Some(diagnostics::router(state)),
        "raw" => Some(raw::router(state)),
        _ => None,
    }
}

/// Resolve a Redis URL from the provided inputs.
///
/// Resolution order:
/// 1. If `url` is provided, use it directly (backward compatible)
/// 2. If `profile` is provided, resolve via profile system
/// 3. Fall back to `state.database_url`
pub(crate) fn resolve_redis_url(
    url: Option<String>,
    profile: Option<&str>,
    state: &AppState,
) -> Result<String, ToolError> {
    if let Some(url) = url {
        return Ok(url);
    }
    if let Some(profile_name) = profile {
        return state
            .database_url_for_profile(Some(profile_name))
            .map_err(|e| {
                ToolError::new(format!(
                    "Failed to resolve database profile '{}': {}",
                    profile_name, e
                ))
            });
    }
    // Try default profile resolution (no explicit profile name)
    if state.database_url.is_none()
        && let Ok(url) = state.database_url_for_profile(None)
    {
        return Ok(url);
    }
    state.database_url.clone().ok_or_else(|| {
        ToolError::new(
            "No Redis URL provided, no profile configured, and no default database URL set",
        )
    })
}

/// Resolve a Redis URL and return a cached multiplexed connection.
///
/// This is the main entry point for tool handlers. It resolves the URL from
/// the input parameters, then returns a pooled connection (creating one if needed).
pub(crate) async fn get_connection(
    url: Option<String>,
    profile: Option<&str>,
    state: &AppState,
) -> Result<redis::aio::MultiplexedConnection, ToolError> {
    let url = resolve_redis_url(url, profile, state)?;
    state
        .redis_connection_for_url(&url)
        .await
        .map_err(|e| ToolError::new(format!("Connection failed: {}", e)))
}

/// Helper to format Redis values for display
pub(crate) fn format_value(v: &redis::Value) -> String {
    match v {
        redis::Value::Nil => "(nil)".to_string(),
        redis::Value::Int(i) => i.to_string(),
        redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
        redis::Value::SimpleString(s) => s.clone(),
        redis::Value::Array(arr) => format!(
            "[{}]",
            arr.iter().map(format_value).collect::<Vec<_>>().join(", ")
        ),
        _ => format!("{:?}", v),
    }
}

/// Build an MCP sub-router containing all Redis database tools
pub fn router(state: Arc<AppState>) -> McpRouter {
    McpRouter::new()
        .merge(server::router(state.clone()))
        .merge(keys::router(state.clone()))
        .merge(structures::router(state.clone()))
        .merge(diagnostics::router(state.clone()))
        .merge(raw::router(state))
}