use std::sync::Arc;
use redis_enterprise::ldap_mappings::LdapMappingHandler;
use redis_enterprise::redis_acls::{CreateRedisAclRequest, RedisAclHandler};
use redis_enterprise::roles::{CreateRoleRequest, RolesHandler};
use redis_enterprise::users::{CreateUserRequest, UpdateUserRequest, UserHandler};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use tower_mcp::extract::{Json, State};
use tower_mcp::{CallToolResult, Error as McpError, McpRouter, ResultExt, Tool, ToolBuilder};
use crate::state::AppState;
use crate::tools::wrap_list;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListUsersInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_users(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_enterprise_users")
.description("List all users in the Redis Enterprise cluster")
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListUsersInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListUsersInput>| 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 = UserHandler::new(client);
let users = handler.list().await.tool_context("Failed to list users")?;
wrap_list("users", &users)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetUserInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn get_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_user")
.description(
"Get detailed information about a specific user in the Redis Enterprise cluster",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetUserInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetUserInput>| 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 = UserHandler::new(client);
let user = handler
.get(input.uid)
.await
.tool_context("Failed to get user")?;
CallToolResult::from_serialize(&user)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateEnterpriseUserInput {
#[serde(default)]
pub profile: Option<String>,
pub email: String,
pub password: String,
pub role: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub email_alerts: Option<bool>,
#[serde(default)]
pub role_uids: Option<Vec<u32>>,
}
pub fn create_enterprise_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("create_enterprise_user")
.description(
"Create a new user in the Redis Enterprise cluster. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, CreateEnterpriseUserInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<CreateEnterpriseUserInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = CreateUserRequest {
email: input.email,
password: input.password,
role: input.role,
name: input.name,
email_alerts: input.email_alerts,
bdbs_email_alerts: None,
role_uids: input.role_uids,
auth_method: None,
};
let handler = UserHandler::new(client);
let user = handler
.create(request)
.await
.tool_context("Failed to create user")?;
CallToolResult::from_serialize(&user)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateEnterpriseUserInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
#[serde(default)]
pub password: Option<String>,
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub email_alerts: Option<bool>,
#[serde(default)]
pub role_uids: Option<Vec<u32>>,
}
pub fn update_enterprise_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_user")
.description(
"Update an existing user in the Redis Enterprise cluster. \
Only specified fields will be modified. Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateEnterpriseUserInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateEnterpriseUserInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = UpdateUserRequest {
password: input.password,
role: input.role,
email: input.email,
name: input.name,
email_alerts: input.email_alerts,
bdbs_email_alerts: None,
role_uids: input.role_uids,
auth_method: None,
};
let handler = UserHandler::new(client);
let user = handler
.update(input.uid, request)
.await
.tool_context("Failed to update user")?;
CallToolResult::from_serialize(&user)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteEnterpriseUserInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn delete_enterprise_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("delete_enterprise_user")
.description(
"DANGEROUS: Permanently deletes a user from the Redis Enterprise cluster. \
Active sessions using this user will be terminated. Requires write permission.",
)
.destructive()
.extractor_handler_typed::<_, _, _, DeleteEnterpriseUserInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DeleteEnterpriseUserInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = UserHandler::new(client);
handler
.delete(input.uid)
.await
.tool_context("Failed to delete user")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "User deleted successfully",
"uid": input.uid
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListRolesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_roles(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_enterprise_roles")
.description(
"List all roles in the Redis Enterprise cluster. Returns role names, \
permissions (management, data_access), and database-specific role assignments.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListRolesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListRolesInput>| 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 = RolesHandler::new(client);
let roles = handler.list().await.tool_context("Failed to list roles")?;
wrap_list("roles", &roles)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetRoleInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn get_role(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_role")
.description(
"Get detailed information about a specific role including permissions \
and database role assignments.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetRoleInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetRoleInput>| 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 = RolesHandler::new(client);
let role = handler
.get(input.uid)
.await
.tool_context("Failed to get role")?;
CallToolResult::from_serialize(&role)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateEnterpriseRoleInput {
#[serde(default)]
pub profile: Option<String>,
pub name: String,
#[serde(default)]
pub management: Option<String>,
#[serde(default)]
pub data_access: Option<String>,
}
pub fn create_enterprise_role(state: Arc<AppState>) -> Tool {
ToolBuilder::new("create_enterprise_role")
.description(
"Create a new role in the Redis Enterprise cluster. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, CreateEnterpriseRoleInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<CreateEnterpriseRoleInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = CreateRoleRequest {
name: input.name,
management: input.management,
data_access: input.data_access,
bdb_roles: None,
cluster_roles: None,
};
let handler = RolesHandler::new(client);
let role = handler
.create(request)
.await
.tool_context("Failed to create role")?;
CallToolResult::from_serialize(&role)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateEnterpriseRoleInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
pub name: String,
#[serde(default)]
pub management: Option<String>,
#[serde(default)]
pub data_access: Option<String>,
}
pub fn update_enterprise_role(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_role")
.description(
"Update an existing role in the Redis Enterprise cluster. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateEnterpriseRoleInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateEnterpriseRoleInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = CreateRoleRequest {
name: input.name,
management: input.management,
data_access: input.data_access,
bdb_roles: None,
cluster_roles: None,
};
let handler = RolesHandler::new(client);
let role = handler
.update(input.uid, request)
.await
.tool_context("Failed to update role")?;
CallToolResult::from_serialize(&role)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteEnterpriseRoleInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn delete_enterprise_role(state: Arc<AppState>) -> Tool {
ToolBuilder::new("delete_enterprise_role")
.description(
"DANGEROUS: Permanently deletes a role from the Redis Enterprise cluster. \
Users assigned to this role will lose their permissions. Requires write permission.",
)
.destructive()
.extractor_handler_typed::<_, _, _, DeleteEnterpriseRoleInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DeleteEnterpriseRoleInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = RolesHandler::new(client);
handler
.delete(input.uid)
.await
.tool_context("Failed to delete role")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Role deleted successfully",
"uid": input.uid
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListRedisAclsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_redis_acls(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_enterprise_acls")
.description(
"List all Redis ACLs in the Redis Enterprise cluster. Returns ACL names, \
rules, and associated databases.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListRedisAclsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListRedisAclsInput>| 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 = RedisAclHandler::new(client);
let acls = handler.list().await.tool_context("Failed to list ACLs")?;
wrap_list("acls", &acls)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetRedisAclInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn get_redis_acl(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_acl")
.description(
"Get detailed information about a specific Redis ACL including the ACL rule string \
and associated databases.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetRedisAclInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetRedisAclInput>| 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 = RedisAclHandler::new(client);
let acl = handler
.get(input.uid)
.await
.tool_context("Failed to get ACL")?;
CallToolResult::from_serialize(&acl)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateEnterpriseAclInput {
#[serde(default)]
pub profile: Option<String>,
pub name: String,
pub acl: String,
#[serde(default)]
pub description: Option<String>,
}
pub fn create_enterprise_acl(state: Arc<AppState>) -> Tool {
ToolBuilder::new("create_enterprise_acl")
.description(
"Create a new Redis ACL in the Redis Enterprise cluster. \
The ACL rule string follows Redis ACL syntax (e.g., \"+@all ~*\"). \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, CreateEnterpriseAclInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<CreateEnterpriseAclInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = CreateRedisAclRequest {
name: input.name,
acl: input.acl,
description: input.description,
};
let handler = RedisAclHandler::new(client);
let acl = handler
.create(request)
.await
.tool_context("Failed to create ACL")?;
CallToolResult::from_serialize(&acl)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateEnterpriseAclInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
pub name: String,
pub acl: String,
#[serde(default)]
pub description: Option<String>,
}
pub fn update_enterprise_acl(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_acl")
.description(
"Update an existing Redis ACL in the Redis Enterprise cluster. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateEnterpriseAclInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateEnterpriseAclInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let request = CreateRedisAclRequest {
name: input.name,
acl: input.acl,
description: input.description,
};
let handler = RedisAclHandler::new(client);
let acl = handler
.update(input.uid, request)
.await
.tool_context("Failed to update ACL")?;
CallToolResult::from_serialize(&acl)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteEnterpriseAclInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn delete_enterprise_acl(state: Arc<AppState>) -> Tool {
ToolBuilder::new("delete_enterprise_acl")
.description(
"DANGEROUS: Permanently deletes a Redis ACL from the cluster. \
Databases using this ACL will lose those access controls. Requires write permission.",
)
.destructive()
.extractor_handler_typed::<_, _, _, DeleteEnterpriseAclInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DeleteEnterpriseAclInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let handler = RedisAclHandler::new(client);
handler
.delete(input.uid)
.await
.tool_context("Failed to delete ACL")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "ACL deleted successfully",
"uid": input.uid
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetLdapConfigInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_enterprise_ldap_config(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_ldap_config")
.description(
"Get the LDAP configuration for the Redis Enterprise cluster including \
server settings, bind DN, and query suffixes.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetLdapConfigInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetLdapConfigInput>| 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 = LdapMappingHandler::new(client);
let config = handler
.get_config()
.await
.tool_context("Failed to get LDAP config")?;
CallToolResult::from_serialize(&config)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateLdapConfigInput {
#[serde(default)]
pub profile: Option<String>,
pub config: Value,
}
pub fn update_enterprise_ldap_config(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_ldap_config")
.description(
"Update the LDAP configuration for the Redis Enterprise cluster. \
Accepts a JSON object with LDAP settings. Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateLdapConfigInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateLdapConfigInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.enterprise_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| crate::tools::credential_error("enterprise", e))?;
let config = serde_json::from_value(input.config)
.tool_context("Invalid LDAP config")?;
let handler = LdapMappingHandler::new(client);
let result = handler
.update_config(config)
.await
.tool_context("Failed to update LDAP config")?;
CallToolResult::from_serialize(&result)
},
)
.build()
}
pub fn router(state: Arc<AppState>) -> McpRouter {
McpRouter::new()
.tool(list_users(state.clone()))
.tool(get_user(state.clone()))
.tool(create_enterprise_user(state.clone()))
.tool(update_enterprise_user(state.clone()))
.tool(delete_enterprise_user(state.clone()))
.tool(list_roles(state.clone()))
.tool(get_role(state.clone()))
.tool(create_enterprise_role(state.clone()))
.tool(update_enterprise_role(state.clone()))
.tool(delete_enterprise_role(state.clone()))
.tool(list_redis_acls(state.clone()))
.tool(get_redis_acl(state.clone()))
.tool(create_enterprise_acl(state.clone()))
.tool(update_enterprise_acl(state.clone()))
.tool(delete_enterprise_acl(state.clone()))
.tool(get_enterprise_ldap_config(state.clone()))
.tool(update_enterprise_ldap_config(state.clone()))
}