use std::sync::Arc;
use redis_enterprise::alerts::AlertHandler;
use redis_enterprise::debuginfo::DebugInfoHandler;
use redis_enterprise::logs::{LogsHandler, LogsQuery};
use redis_enterprise::modules::ModuleHandler;
use redis_enterprise::shards::ShardHandler;
use redis_enterprise::stats::StatsHandler;
use schemars::JsonSchema;
use serde::Deserialize;
use tower_mcp::extract::{Json, State};
use tower_mcp::{CallToolResult, McpRouter, ResultExt, Tool, ToolBuilder};
use crate::state::AppState;
use crate::tools::wrap_list;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListAlertsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_alerts(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_alerts")
.description("List all active alerts in the Redis Enterprise cluster")
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListAlertsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListAlertsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = AlertHandler::new(client);
let alerts = handler.list().await.tool_context("Failed to list alerts")?;
wrap_list("alerts", &alerts)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListLogsInput {
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub start_time: Option<String>,
#[serde(default)]
pub end_time: Option<String>,
#[serde(default)]
pub order: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
#[serde(default)]
pub offset: Option<u32>,
}
pub fn list_logs(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_logs")
.description(
"List cluster event logs from Redis Enterprise. Logs include events like database \
changes, node status updates, configuration modifications, and alerts. Supports \
filtering by time range and pagination.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListLogsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListLogsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let query = if input.start_time.is_some()
|| input.end_time.is_some()
|| input.order.is_some()
|| input.limit.is_some()
|| input.offset.is_some()
{
Some(LogsQuery {
stime: input.start_time,
etime: input.end_time,
order: input.order,
limit: input.limit,
offset: input.offset,
})
} else {
None
};
let handler = LogsHandler::new(client);
let logs = handler
.list(query)
.await
.tool_context("Failed to list logs")?;
wrap_list("logs", &logs)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAllNodesStatsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_all_nodes_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_all_nodes_stats")
.description(
"Get current statistics for all nodes in the Redis Enterprise cluster in a single \
call. Returns aggregated stats per node including CPU, memory, and network metrics.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetAllNodesStatsInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetAllNodesStatsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = StatsHandler::new(client);
let stats = handler
.nodes_last()
.await
.tool_context("Failed to get all nodes stats")?;
CallToolResult::from_serialize(&stats)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAllDatabasesStatsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_all_databases_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_all_databases_stats")
.description(
"Get current statistics for all databases in the Redis Enterprise cluster in a \
single call. Returns aggregated stats per database including latency, throughput, \
and memory usage.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetAllDatabasesStatsInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetAllDatabasesStatsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = StatsHandler::new(client);
let stats = handler.databases_last().await.tool_context("Failed to get all databases stats")?;
CallToolResult::from_serialize(&stats)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetShardStatsInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn get_shard_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_shard_stats")
.description("Get current statistics for a specific shard in the Redis Enterprise cluster")
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetShardStatsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetShardStatsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = StatsHandler::new(client);
let stats = handler
.shard(input.uid, None)
.await
.tool_context("Failed to get shard stats")?;
CallToolResult::from_serialize(&stats)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAllShardsStatsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_all_shards_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_all_shards_stats")
.description(
"Get current statistics for all shards in the Redis Enterprise cluster in a single \
call. Returns aggregated stats per shard.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetAllShardsStatsInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetAllShardsStatsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = StatsHandler::new(client);
let stats = handler
.shards(None)
.await
.tool_context("Failed to get all shards stats")?;
CallToolResult::from_serialize(&stats)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListShardsInput {
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub database_uid: Option<u32>,
}
pub fn list_shards(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_shards")
.description(
"List all shards in the Redis Enterprise cluster. Optionally filter by database UID.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListShardsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListShardsInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = ShardHandler::new(client);
let shards = if let Some(db_uid) = input.database_uid {
handler
.list_by_database(db_uid)
.await
.tool_context("Failed to list shards")?
} else {
handler.list().await.tool_context("Failed to list shards")?
};
wrap_list("shards", &shards)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetShardInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: String,
}
pub fn get_shard(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_shard")
.description(
"Get detailed information about a specific shard in the Redis Enterprise cluster \
including role (master/replica), status, and assigned node.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetShardInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetShardInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = ShardHandler::new(client);
let shard = handler
.get(&input.uid)
.await
.tool_context("Failed to get shard")?;
CallToolResult::from_serialize(&shard)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListDebugInfoTasksInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_debug_info_tasks(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_debug_info_tasks")
.description(
"List all debug info collection tasks in the Redis Enterprise cluster. Returns task \
IDs, statuses (queued, running, completed, failed), and download URLs for completed \
collections.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListDebugInfoTasksInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<ListDebugInfoTasksInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = DebugInfoHandler::new(client);
let tasks = handler
.list()
.await
.tool_context("Failed to list debug info tasks")?;
wrap_list("tasks", &tasks)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetDebugInfoStatusInput {
#[serde(default)]
pub profile: Option<String>,
pub task_id: String,
}
pub fn get_debug_info_status(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_debug_info_status")
.description(
"Get the status of a debug info collection task. Returns status (queued, running, \
completed, failed), progress percentage, download URL (when completed), and error \
message (if failed).",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetDebugInfoStatusInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetDebugInfoStatusInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = DebugInfoHandler::new(client);
let status = handler
.status(&input.task_id)
.await
.tool_context("Failed to get debug info status")?;
CallToolResult::from_serialize(&status)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListModulesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_modules(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_modules")
.description(
"List all Redis modules installed on the Redis Enterprise cluster. Returns module \
names, versions, descriptions, and capabilities (e.g., RedisJSON, RediSearch, \
RedisTimeSeries).",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListModulesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListModulesInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = ModuleHandler::new(client);
let modules = handler
.list()
.await
.tool_context("Failed to list modules")?;
wrap_list("modules", &modules)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetModuleInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: String,
}
pub fn get_module(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_module")
.description(
"Get detailed information about a specific Redis module including version, \
description, author, license, capabilities, and platform compatibility.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetModuleInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetModuleInput>| async move {
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = ModuleHandler::new(client);
let module = handler
.get(&input.uid)
.await
.tool_context("Failed to get module")?;
CallToolResult::from_serialize(&module)
},
)
.build()
}
pub fn router(state: Arc<AppState>) -> McpRouter {
McpRouter::new()
.tool(list_alerts(state.clone()))
.tool(list_logs(state.clone()))
.tool(get_all_nodes_stats(state.clone()))
.tool(get_all_databases_stats(state.clone()))
.tool(get_shard_stats(state.clone()))
.tool(get_all_shards_stats(state.clone()))
.tool(list_shards(state.clone()))
.tool(get_shard(state.clone()))
.tool(list_debug_info_tasks(state.clone()))
.tool(get_debug_info_status(state.clone()))
.tool(list_modules(state.clone()))
.tool(get_module(state.clone()))
}