use std::sync::Arc;
use std::time::Duration;
use redis_cloud::databases::DatabaseCreateRequest;
use redis_cloud::flexible::{DatabaseHandler, SubscriptionHandler};
use redis_cloud::{AccountHandler, AclHandler, TaskHandler, UserHandler};
use redisctl_core::cloud::{
backup_database_and_wait, create_database_and_wait, delete_database_and_wait,
delete_subscription_and_wait, flush_database_and_wait, import_database_and_wait,
update_database_and_wait,
};
use schemars::JsonSchema;
use serde::Deserialize;
use tower_mcp::extract::{Json, State};
use tower_mcp::{CallToolResult, Error as McpError, McpRouter, Tool, ToolBuilder, ToolError};
use crate::state::AppState;
use crate::tools::wrap_list;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListSubscriptionsInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_subscriptions(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_subscriptions")
.description("List all Redis Cloud subscriptions accessible with the current credentials. Returns JSON with subscription details.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListSubscriptionsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListSubscriptionsInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = SubscriptionHandler::new(client);
let account_subs = handler
.get_all_subscriptions()
.await
.map_err(|e| ToolError::new(format!("Failed to list subscriptions: {}", e)))?;
CallToolResult::from_serialize(&account_subs)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetSubscriptionInput {
pub subscription_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_subscription(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_subscription")
.description("Get detailed information about a specific Redis Cloud subscription. Returns JSON with full subscription details.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetSubscriptionInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetSubscriptionInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = SubscriptionHandler::new(client);
let subscription = handler
.get_subscription_by_id(input.subscription_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get subscription: {}", e)))?;
CallToolResult::from_serialize(&subscription)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListDatabasesInput {
pub subscription_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_databases(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_databases")
.description(
"List all databases in a Redis Cloud subscription. Returns JSON with database details.",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListDatabasesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListDatabasesInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let databases = handler
.get_subscription_databases(input.subscription_id, None, None)
.await
.map_err(|e| ToolError::new(format!("Failed to list databases: {}", e)))?;
CallToolResult::from_serialize(&databases)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_database")
.description("Get detailed information about a specific Redis Cloud database. Returns JSON with full database configuration.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetDatabaseInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetDatabaseInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let database = handler
.get_subscription_database_by_id(input.subscription_id, input.database_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get database: {}", e)))?;
CallToolResult::from_serialize(&database)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAccountInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_account(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_account")
.description("Get information about the current Redis Cloud account including name, ID, and settings.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetAccountInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetAccountInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AccountHandler::new(client);
let account = handler
.get_current_account()
.await
.map_err(|e| ToolError::new(format!("Failed to get account: {}", e)))?;
CallToolResult::from_serialize(&account)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetSystemLogsInput {
#[serde(default)]
pub offset: Option<i32>,
#[serde(default)]
pub limit: Option<i32>,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_system_logs(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_system_logs")
.description(
"Get system audit logs for the Redis Cloud account. Includes events like \
subscription changes, database modifications, and user actions.",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetSystemLogsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetSystemLogsInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AccountHandler::new(client);
let logs = handler
.get_account_system_logs(input.offset, input.limit)
.await
.map_err(|e| ToolError::new(format!("Failed to get system logs: {}", e)))?;
CallToolResult::from_serialize(&logs)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetSessionLogsInput {
#[serde(default)]
pub offset: Option<i32>,
#[serde(default)]
pub limit: Option<i32>,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_session_logs(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_session_logs")
.description(
"Get session activity logs for the Redis Cloud account. Includes user login/logout \
events and session information.",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetSessionLogsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetSessionLogsInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AccountHandler::new(client);
let logs = handler
.get_account_session_logs(input.offset, input.limit)
.await
.map_err(|e| ToolError::new(format!("Failed to get session logs: {}", e)))?;
CallToolResult::from_serialize(&logs)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetRegionsInput {
#[serde(default)]
pub provider: Option<String>,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_regions(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_regions")
.description(
"Get supported cloud regions for Redis Cloud. Optionally filter by provider (AWS, GCP, Azure).",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetRegionsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetRegionsInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AccountHandler::new(client);
let regions = handler
.get_supported_regions(input.provider)
.await
.map_err(|e| ToolError::new(format!("Failed to get regions: {}", e)))?;
CallToolResult::from_serialize(®ions)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetModulesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_modules(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_modules")
.description(
"Get supported Redis database modules (e.g., Search, JSON, TimeSeries, Bloom).",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetModulesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetModulesInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AccountHandler::new(client);
let modules = handler
.get_supported_database_modules()
.await
.map_err(|e| ToolError::new(format!("Failed to get modules: {}", e)))?;
CallToolResult::from_serialize(&modules)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListTasksInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_tasks(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_tasks")
.description("List all async tasks in the Redis Cloud account. Tasks track long-running operations like database creation.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListTasksInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListTasksInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = TaskHandler::new(client);
let tasks = handler
.get_all_tasks()
.await
.map_err(|e| ToolError::new(format!("Failed to list tasks: {}", e)))?;
wrap_list("tasks", &tasks)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetTaskInput {
pub task_id: String,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_task(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_task")
.description("Get status and details of a specific async task by ID.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetTaskInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetTaskInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = TaskHandler::new(client);
let task = handler
.get_task_by_id(input.task_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get task: {}", e)))?;
CallToolResult::from_serialize(&task)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListAccountUsersInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_account_users(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_account_users")
.description(
"List all users in the Redis Cloud account (team members with console access).",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListAccountUsersInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListAccountUsersInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = UserHandler::new(client);
let users = handler
.get_all_users()
.await
.map_err(|e| ToolError::new(format!("Failed to list users: {}", e)))?;
CallToolResult::from_serialize(&users)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAccountUserInput {
pub user_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_account_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_account_user")
.description("Get detailed information about a specific account user (team member) by ID.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetAccountUserInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetAccountUserInput>| async move {
let client = state.cloud_client_for_profile(input.profile.as_deref()).await.map_err(|e| {
ToolError::new(format!("Failed to get Cloud client: {}", e))
})?;
let handler = UserHandler::new(client);
let user = handler
.get_user_by_id(input.user_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get account user: {}", e)))?;
CallToolResult::from_serialize(&user)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListAclUsersInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_acl_users(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_acl_users")
.description("List all ACL users (database-level Redis users for authentication).")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListAclUsersInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListAclUsersInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AclHandler::new(client);
let users = handler
.get_all_acl_users()
.await
.map_err(|e| ToolError::new(format!("Failed to list ACL users: {}", e)))?;
CallToolResult::from_serialize(&users)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetAclUserInput {
pub user_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_acl_user(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_acl_user")
.description("Get detailed information about a specific ACL user by ID.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetAclUserInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetAclUserInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AclHandler::new(client);
let user = handler
.get_user_by_id(input.user_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get ACL user: {}", e)))?;
CallToolResult::from_serialize(&user)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListAclRolesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_acl_roles(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_acl_roles")
.description("List all ACL roles (permission templates for database access).")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListAclRolesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListAclRolesInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AclHandler::new(client);
let roles = handler
.get_roles()
.await
.map_err(|e| ToolError::new(format!("Failed to list ACL roles: {}", e)))?;
CallToolResult::from_serialize(&roles)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListRedisRulesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_redis_rules(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_redis_rules")
.description("List all Redis ACL rules (command permissions for Redis users).")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, ListRedisRulesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListRedisRulesInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = AclHandler::new(client);
let rules = handler
.get_all_redis_rules()
.await
.map_err(|e| ToolError::new(format!("Failed to list Redis rules: {}", e)))?;
CallToolResult::from_serialize(&rules)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetBackupStatusInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub region_name: Option<String>,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_backup_status(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_backup_status")
.description("Get backup status and history for a Redis Cloud database.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetBackupStatusInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetBackupStatusInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let status = handler
.get_database_backup_status(
input.subscription_id,
input.database_id,
input.region_name,
)
.await
.map_err(|e| ToolError::new(format!("Failed to get backup status: {}", e)))?;
CallToolResult::from_serialize(&status)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetSlowLogInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub region_name: Option<String>,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_slow_log(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_slow_log")
.description(
"Get slow log entries for a Redis Cloud database. Shows slow queries for debugging.",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetSlowLogInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetSlowLogInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let log = handler
.get_slow_log(input.subscription_id, input.database_id, input.region_name)
.await
.map_err(|e| ToolError::new(format!("Failed to get slow log: {}", e)))?;
CallToolResult::from_serialize(&log)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetTagsInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_tags(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_database_tags")
.description("Get tags attached to a Redis Cloud database.")
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetTagsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetTagsInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let tags = handler
.get_tags(input.subscription_id, input.database_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get tags: {}", e)))?;
CallToolResult::from_serialize(&tags)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetCertificateInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_database_certificate(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_database_certificate")
.description(
"Get the TLS/SSL certificate for a Redis Cloud database. \
Returns the public certificate in PEM format for TLS connections.",
)
.read_only()
.idempotent()
.extractor_handler_typed::<_, _, _, GetCertificateInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetCertificateInput>| async move {
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let handler = DatabaseHandler::new(client);
let cert = handler
.get_subscription_database_certificate(input.subscription_id, input.database_id)
.await
.map_err(|e| ToolError::new(format!("Failed to get certificate: {}", e)))?;
CallToolResult::from_serialize(&cert)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateDatabaseInput {
pub subscription_id: i32,
pub name: String,
pub memory_limit_in_gb: f64,
#[serde(default = "default_replication")]
pub replication: bool,
#[serde(default = "default_protocol")]
pub protocol: String,
#[serde(default)]
pub data_persistence: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
fn default_replication() -> bool {
true
}
fn default_protocol() -> String {
"redis".to_string()
}
fn default_timeout() -> u64 {
600
}
pub fn create_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("create_database")
.description(
"Create a new Redis Cloud database and wait for it to be ready. \
Returns the created database details. Requires write permission.",
)
.extractor_handler_typed::<_, _, _, CreateDatabaseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<CreateDatabaseInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let request = match (input.protocol.as_str(), input.data_persistence.as_ref()) {
("redis", None) => DatabaseCreateRequest::builder()
.name(&input.name)
.memory_limit_in_gb(input.memory_limit_in_gb)
.replication(input.replication)
.build(),
("redis", Some(persistence)) => DatabaseCreateRequest::builder()
.name(&input.name)
.memory_limit_in_gb(input.memory_limit_in_gb)
.replication(input.replication)
.data_persistence(persistence)
.build(),
(protocol, None) => DatabaseCreateRequest::builder()
.name(&input.name)
.memory_limit_in_gb(input.memory_limit_in_gb)
.replication(input.replication)
.protocol(protocol)
.build(),
(protocol, Some(persistence)) => DatabaseCreateRequest::builder()
.name(&input.name)
.memory_limit_in_gb(input.memory_limit_in_gb)
.replication(input.replication)
.protocol(protocol)
.data_persistence(persistence)
.build(),
};
let database = create_database_and_wait(
&client,
input.subscription_id,
&request,
Duration::from_secs(input.timeout_seconds),
None, )
.await
.map_err(|e| ToolError::new(format!("Failed to create database: {}", e)))?;
CallToolResult::from_serialize(&database)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub memory_limit_in_gb: Option<f64>,
#[serde(default)]
pub replication: Option<bool>,
#[serde(default)]
pub data_persistence: Option<String>,
#[serde(default)]
pub data_eviction_policy: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
pub fn update_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_database")
.description(
"Update an existing Redis Cloud database configuration. \
Returns the updated database details. Requires write permission.",
)
.extractor_handler_typed::<_, _, _, UpdateDatabaseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateDatabaseInput>| async move {
use redis_cloud::databases::DatabaseUpdateRequest;
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let mut request = DatabaseUpdateRequest::builder().build();
request.name = input.name;
request.memory_limit_in_gb = input.memory_limit_in_gb;
request.replication = input.replication;
request.data_persistence = input.data_persistence;
request.data_eviction_policy = input.data_eviction_policy;
if request.name.is_none()
&& request.memory_limit_in_gb.is_none()
&& request.replication.is_none()
&& request.data_persistence.is_none()
&& request.data_eviction_policy.is_none()
{
return Err(McpError::tool(
"At least one update field is required",
));
}
let database = update_database_and_wait(
&client,
input.subscription_id,
input.database_id,
&request,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to update database: {}", e)))?;
CallToolResult::from_serialize(&database)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
pub fn delete_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("delete_database")
.description(
"Delete a Redis Cloud database. This is a destructive operation. \
Requires write permission.",
)
.extractor_handler_typed::<_, _, _, DeleteDatabaseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DeleteDatabaseInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
delete_database_and_wait(
&client,
input.subscription_id,
input.database_id,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to delete database: {}", e)))?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Database deleted successfully",
"subscription_id": input.subscription_id,
"database_id": input.database_id
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct BackupDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default)]
pub region_name: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
pub fn backup_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("backup_database")
.description(
"Trigger a manual backup of a Redis Cloud database. \
Requires write permission.",
)
.extractor_handler_typed::<_, _, _, BackupDatabaseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<BackupDatabaseInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
backup_database_and_wait(
&client,
input.subscription_id,
input.database_id,
input.region_name.as_deref(),
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to backup database: {}", e)))?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Backup completed successfully",
"subscription_id": input.subscription_id,
"database_id": input.database_id
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ImportDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
pub source_type: String,
pub import_from_uri: String,
#[serde(default = "default_import_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
fn default_import_timeout() -> u64 {
1800 }
pub fn import_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("import_database")
.description(
"Import data into a Redis Cloud database from an external source. \
WARNING: This will overwrite existing data. Requires write permission.",
)
.extractor_handler_typed::<_, _, _, ImportDatabaseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<ImportDatabaseInput>| async move {
use redis_cloud::databases::DatabaseImportRequest;
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let request = DatabaseImportRequest::builder()
.source_type(&input.source_type)
.import_from_uri(vec![input.import_from_uri.clone()])
.build();
import_database_and_wait(
&client,
input.subscription_id,
input.database_id,
&request,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to import database: {}", e)))?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Import completed successfully",
"subscription_id": input.subscription_id,
"database_id": input.database_id
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteSubscriptionInput {
pub subscription_id: i32,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
pub fn delete_subscription(state: Arc<AppState>) -> Tool {
ToolBuilder::new("delete_subscription")
.description(
"Delete a Redis Cloud subscription. WARNING: All databases in the subscription \
must be deleted first. This is a destructive operation. Requires write permission.",
)
.extractor_handler_typed::<_, _, _, DeleteSubscriptionInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DeleteSubscriptionInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
delete_subscription_and_wait(
&client,
input.subscription_id,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to delete subscription: {}", e)))?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Subscription deleted successfully",
"subscription_id": input.subscription_id
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct FlushDatabaseInput {
pub subscription_id: i32,
pub database_id: i32,
#[serde(default = "default_flush_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
fn default_flush_timeout() -> u64 {
300
}
pub fn flush_database(state: Arc<AppState>) -> Tool {
ToolBuilder::new("flush_database")
.description(
"Flush all data from a Redis Cloud database and wait for completion. \
WARNING: This permanently deletes ALL data in the database! \
Requires write permission.",
)
.extractor_handler_typed::<_, _, _, FlushDatabaseInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<FlushDatabaseInput>| async move {
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
flush_database_and_wait(
&client,
input.subscription_id,
input.database_id,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to flush database: {}", e)))?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Database flushed successfully",
"subscription_id": input.subscription_id,
"database_id": input.database_id
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateSubscriptionInput {
pub name: String,
pub cloud_provider: String,
pub region: String,
#[serde(default = "default_cloud_account_id")]
pub cloud_account_id: i32,
pub database_name: String,
pub memory_limit_in_gb: f64,
#[serde(default = "default_protocol")]
pub protocol: String,
#[serde(default = "default_replication")]
pub replication: bool,
#[serde(default = "default_subscription_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub profile: Option<String>,
}
fn default_cloud_account_id() -> i32 {
1 }
fn default_subscription_timeout() -> u64 {
1800 }
pub fn create_subscription(state: Arc<AppState>) -> Tool {
ToolBuilder::new("create_subscription")
.description(
"Create a new Redis Cloud Pro subscription with an initial database. \
This is a simplified interface for common subscription creation scenarios. \
Requires write permission.",
)
.extractor_handler_typed::<_, _, _, CreateSubscriptionInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<CreateSubscriptionInput>| async move {
use redis_cloud::flexible::subscriptions::{
SubscriptionCreateRequest, SubscriptionDatabaseSpec, SubscriptionRegionSpec,
SubscriptionSpec,
};
use redisctl_core::cloud::create_subscription_and_wait;
if !state.is_write_allowed() {
return Err(McpError::tool(
"Write operations not allowed in read-only mode",
));
}
let client = state
.cloud_client_for_profile(input.profile.as_deref())
.await
.map_err(|e| ToolError::new(format!("Failed to get Cloud client: {}", e)))?;
let request = SubscriptionCreateRequest::builder()
.name(&input.name)
.cloud_providers(vec![SubscriptionSpec {
provider: Some(input.cloud_provider.clone()),
cloud_account_id: Some(input.cloud_account_id),
regions: vec![SubscriptionRegionSpec {
region: input.region.clone(),
multiple_availability_zones: None,
preferred_availability_zones: None,
networking: None,
}],
}])
.databases(vec![SubscriptionDatabaseSpec {
name: input.database_name.clone(),
protocol: input.protocol.clone(),
memory_limit_in_gb: Some(input.memory_limit_in_gb),
dataset_size_in_gb: None,
support_oss_cluster_api: None,
data_persistence: None,
replication: Some(input.replication),
throughput_measurement: None,
local_throughput_measurement: None,
modules: None,
quantity: None,
average_item_size_in_bytes: None,
resp_version: None,
redis_version: None,
sharding_type: None,
query_performance_factor: None,
}])
.build();
let subscription = create_subscription_and_wait(
&client,
&request,
Duration::from_secs(input.timeout_seconds),
None,
)
.await
.map_err(|e| ToolError::new(format!("Failed to create subscription: {}", e)))?;
CallToolResult::from_serialize(&subscription)
},
)
.build()
}
pub fn instructions() -> &'static str {
r#"
### Redis Cloud - Subscriptions & Databases
- list_subscriptions: List all Cloud subscriptions
- get_subscription: Get subscription details
- list_databases: List databases in a subscription
- get_database: Get database details
- get_backup_status: Get database backup status
- get_slow_log: Get slow query log
- get_database_tags: Get tags for a database
- get_database_certificate: Get TLS/SSL certificate for a database
### Redis Cloud - Account & Configuration
- get_account: Get current account information
- get_regions: Get supported cloud regions
- get_modules: Get supported Redis modules
- list_account_users: List team members
- get_account_user: Get team member details by ID
- list_acl_users: List database ACL users
- get_acl_user: Get ACL user details by ID
- list_acl_roles: List ACL roles
- list_redis_rules: List Redis ACL rules
### Redis Cloud - Logs
- get_system_logs: Get system audit logs (subscription/database changes)
- get_session_logs: Get session activity logs (login/logout events)
### Redis Cloud - Tasks
- list_tasks: List async operations
- get_task: Get task status
### Redis Cloud - Write Operations (require --read-only=false)
- create_database: Create a new database and wait for it to be ready
- update_database: Update a database configuration
- delete_database: Delete a database
- backup_database: Trigger a manual backup
- import_database: Import data into a database
- delete_subscription: Delete a subscription (all databases must be deleted first)
- flush_database: Flush all data from a database
"#
}
pub fn router(state: Arc<AppState>) -> McpRouter {
McpRouter::new()
.tool(list_subscriptions(state.clone()))
.tool(get_subscription(state.clone()))
.tool(list_databases(state.clone()))
.tool(get_database(state.clone()))
.tool(get_backup_status(state.clone()))
.tool(get_slow_log(state.clone()))
.tool(get_tags(state.clone()))
.tool(get_database_certificate(state.clone()))
.tool(get_account(state.clone()))
.tool(get_regions(state.clone()))
.tool(get_modules(state.clone()))
.tool(list_account_users(state.clone()))
.tool(get_account_user(state.clone()))
.tool(list_acl_users(state.clone()))
.tool(get_acl_user(state.clone()))
.tool(list_acl_roles(state.clone()))
.tool(list_redis_rules(state.clone()))
.tool(get_system_logs(state.clone()))
.tool(get_session_logs(state.clone()))
.tool(list_tasks(state.clone()))
.tool(get_task(state.clone()))
.tool(create_database(state.clone()))
.tool(update_database(state.clone()))
.tool(delete_database(state.clone()))
.tool(backup_database(state.clone()))
.tool(import_database(state.clone()))
.tool(delete_subscription(state.clone()))
.tool(flush_database(state.clone()))
.tool(create_subscription(state.clone()))
}