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;
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,
},
];
pub fn tool_names() -> Vec<String> {
SUB_MODULES
.iter()
.flat_map(|sm| sm.tool_names.iter().map(|s| (*s).to_string()))
.collect()
}
pub fn sub_tool_names(name: &str) -> Option<&'static [&'static str]> {
SUB_MODULES
.iter()
.find(|sm| sm.name == name)
.map(|sm| sm.tool_names)
}
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,
}
}
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
))
});
}
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",
)
})
}
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)))
}
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),
}
}
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))
}