use std::sync::Arc;
use schemars::JsonSchema;
use serde::Deserialize;
use tower_mcp::{CallToolResult, Tool, ToolBuilder, ToolError};
use crate::state::AppState;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct PingInput {
#[serde(default)]
pub url: Option<String>,
}
pub fn ping(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_ping")
.description("Test connectivity to a Redis database by sending a PING command")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: PingInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let response: String = redis::cmd("PING")
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("PING failed: {}", e)))?;
Ok(CallToolResult::text(format!(
"Connected successfully. Response: {}",
response
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct InfoInput {
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub section: Option<String>,
}
pub fn info(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_info")
.description("Get Redis server information using the INFO command")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: InfoInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let mut cmd = redis::cmd("INFO");
if let Some(section) = &input.section {
cmd.arg(section);
}
let info: String = cmd
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("INFO failed: {}", e)))?;
Ok(CallToolResult::text(info))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct KeysInput {
#[serde(default)]
pub url: Option<String>,
#[serde(default = "default_pattern")]
pub pattern: String,
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_pattern() -> String {
"*".to_string()
}
fn default_limit() -> usize {
100
}
pub fn keys(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_keys")
.description(
"List keys matching a pattern using SCAN (production-safe, non-blocking). \
Returns up to 'limit' keys.",
)
.read_only()
.idempotent()
.handler_with_state(state, |state, input: KeysInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let mut cursor: u64 = 0;
let mut all_keys: Vec<String> = Vec::new();
loop {
let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
.arg(cursor)
.arg("MATCH")
.arg(&input.pattern)
.arg("COUNT")
.arg(100)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("SCAN failed: {}", e)))?;
all_keys.extend(keys);
cursor = new_cursor;
if cursor == 0 || all_keys.len() >= input.limit {
break;
}
}
all_keys.truncate(input.limit);
let output = if all_keys.is_empty() {
format!("No keys found matching pattern '{}'", input.pattern)
} else {
format!(
"Found {} key(s) matching '{}'\n\n{}",
all_keys.len(),
input.pattern,
all_keys.join("\n")
)
};
Ok(CallToolResult::text(output))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn get(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_get")
.description("Get the value of a key from Redis")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: GetInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let value: Option<String> = redis::cmd("GET")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("GET failed: {}", e)))?;
match value {
Some(v) => Ok(CallToolResult::text(v)),
None => Ok(CallToolResult::text(format!(
"(nil) - key '{}' not found",
input.key
))),
}
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct TypeInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn key_type(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_type")
.description("Get the type of a key (string, list, set, zset, hash, stream)")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: TypeInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let key_type: String = redis::cmd("TYPE")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("TYPE failed: {}", e)))?;
Ok(CallToolResult::text(format!("{}: {}", input.key, key_type)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct TtlInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn ttl(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_ttl")
.description("Get the time-to-live (TTL) of a key in seconds. Returns -1 if no expiry, -2 if key doesn't exist.")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: TtlInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let ttl: i64 = redis::cmd("TTL")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("TTL failed: {}", e)))?;
let message = match ttl {
-2 => format!("{}: key does not exist", input.key),
-1 => format!("{}: no expiry set", input.key),
_ => format!("{}: {} seconds remaining", input.key, ttl),
};
Ok(CallToolResult::text(message))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ExistsInput {
#[serde(default)]
pub url: Option<String>,
pub keys: Vec<String>,
}
pub fn exists(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_exists")
.description("Check if one or more keys exist. Returns the count of keys that exist.")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: ExistsInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let mut cmd = redis::cmd("EXISTS");
for key in &input.keys {
cmd.arg(key);
}
let count: i64 = cmd
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("EXISTS failed: {}", e)))?;
Ok(CallToolResult::text(format!(
"{} of {} key(s) exist",
count,
input.keys.len()
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DbsizeInput {
#[serde(default)]
pub url: Option<String>,
}
pub fn dbsize(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_dbsize")
.description("Get the number of keys in the currently selected database")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: DbsizeInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let size: i64 = redis::cmd("DBSIZE")
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("DBSIZE failed: {}", e)))?;
Ok(CallToolResult::text(format!(
"Database contains {} keys",
size
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct MemoryUsageInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn memory_usage(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_memory_usage")
.description("Get the memory usage of a key in bytes")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: MemoryUsageInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let bytes: Option<i64> = redis::cmd("MEMORY")
.arg("USAGE")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("MEMORY USAGE failed: {}", e)))?;
match bytes {
Some(b) => Ok(CallToolResult::text(format!("{}: {} bytes", input.key, b))),
None => Ok(CallToolResult::text(format!(
"{}: key does not exist",
input.key
))),
}
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct HgetallInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn hgetall(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_hgetall")
.description("Get all fields and values from a hash")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: HgetallInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let result: Vec<(String, String)> = redis::cmd("HGETALL")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("HGETALL failed: {}", e)))?;
if result.is_empty() {
return Ok(CallToolResult::text(format!(
"(empty hash or key '{}' not found)",
input.key
)));
}
let output = result
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<_>>()
.join("\n");
Ok(CallToolResult::text(format!(
"Hash '{}' ({} fields):\n{}",
input.key,
result.len(),
output
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct LrangeInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
#[serde(default)]
pub start: i64,
#[serde(default = "default_stop")]
pub stop: i64,
}
fn default_stop() -> i64 {
-1
}
pub fn lrange(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_lrange")
.description("Get a range of elements from a list. Use start=0, stop=-1 for all elements.")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: LrangeInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let result: Vec<String> = redis::cmd("LRANGE")
.arg(&input.key)
.arg(input.start)
.arg(input.stop)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("LRANGE failed: {}", e)))?;
if result.is_empty() {
return Ok(CallToolResult::text(format!(
"(empty list or key '{}' not found)",
input.key
)));
}
let output = result
.iter()
.enumerate()
.map(|(i, v)| format!("{}: {}", i, v))
.collect::<Vec<_>>()
.join("\n");
Ok(CallToolResult::text(format!(
"List '{}' ({} elements):\n{}",
input.key,
result.len(),
output
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SmembersInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
}
pub fn smembers(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_smembers")
.description("Get all members of a set")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: SmembersInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let result: Vec<String> = redis::cmd("SMEMBERS")
.arg(&input.key)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("SMEMBERS failed: {}", e)))?;
if result.is_empty() {
return Ok(CallToolResult::text(format!(
"(empty set or key '{}' not found)",
input.key
)));
}
Ok(CallToolResult::text(format!(
"Set '{}' ({} members):\n{}",
input.key,
result.len(),
result.join("\n")
)))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ZrangeInput {
#[serde(default)]
pub url: Option<String>,
pub key: String,
#[serde(default)]
pub start: i64,
#[serde(default = "default_stop")]
pub stop: i64,
#[serde(default)]
pub withscores: bool,
}
pub fn zrange(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_zrange")
.description("Get a range of members from a sorted set by index. Use withscores=true to include scores.")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: ZrangeInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
if input.withscores {
let result: Vec<(String, f64)> = redis::cmd("ZRANGE")
.arg(&input.key)
.arg(input.start)
.arg(input.stop)
.arg("WITHSCORES")
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("ZRANGE failed: {}", e)))?;
if result.is_empty() {
return Ok(CallToolResult::text(format!(
"(empty sorted set or key '{}' not found)",
input.key
)));
}
let output = result
.iter()
.enumerate()
.map(|(i, (member, score))| format!("{}: {} (score: {})", i, member, score))
.collect::<Vec<_>>()
.join("\n");
Ok(CallToolResult::text(format!(
"Sorted set '{}' ({} members):\n{}",
input.key,
result.len(),
output
)))
} else {
let result: Vec<String> = redis::cmd("ZRANGE")
.arg(&input.key)
.arg(input.start)
.arg(input.stop)
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("ZRANGE failed: {}", e)))?;
if result.is_empty() {
return Ok(CallToolResult::text(format!(
"(empty sorted set or key '{}' not found)",
input.key
)));
}
let output = result
.iter()
.enumerate()
.map(|(i, v)| format!("{}: {}", i, v))
.collect::<Vec<_>>()
.join("\n");
Ok(CallToolResult::text(format!(
"Sorted set '{}' ({} members):\n{}",
input.key,
result.len(),
output
)))
}
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ClusterInfoInput {
#[serde(default)]
pub url: Option<String>,
}
pub fn cluster_info(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_cluster_info")
.description("Get Redis Cluster information (only works on cluster-enabled databases)")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: ClusterInfoInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let info: String = redis::cmd("CLUSTER")
.arg("INFO")
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("CLUSTER INFO failed: {}", e)))?;
Ok(CallToolResult::text(info))
})
.build()
.expect("valid tool")
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ClientListInput {
#[serde(default)]
pub url: Option<String>,
}
pub fn client_list(state: Arc<AppState>) -> Tool {
ToolBuilder::new("redis_client_list")
.description("Get list of client connections to the Redis server")
.read_only()
.idempotent()
.handler_with_state(state, |state, input: ClientListInput| async move {
let url = input
.url
.or_else(|| state.database_url.clone())
.ok_or_else(|| ToolError::new("No Redis URL provided or configured"))?;
let client = redis::Client::open(url.as_str())
.map_err(|e| ToolError::new(format!("Invalid URL: {}", e)))?;
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| ToolError::new(format!("Connection failed: {}", e)))?;
let clients: String = redis::cmd("CLIENT")
.arg("LIST")
.query_async(&mut conn)
.await
.map_err(|e| ToolError::new(format!("CLIENT LIST failed: {}", e)))?;
let count = clients.lines().count();
Ok(CallToolResult::text(format!(
"{} connected client(s):\n\n{}",
count, clients
)))
})
.build()
.expect("valid tool")
}