use std::sync::Arc;
use redis_enterprise::cluster::ClusterHandler;
use redis_enterprise::license::{LicenseHandler, LicenseUpdateRequest};
use redis_enterprise::nodes::NodeHandler;
use redis_enterprise::stats::{StatsHandler, StatsQuery};
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;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetClusterInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_cluster(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_cluster")
.description(
"Get Redis Enterprise cluster information including name, version, and configuration",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetClusterInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetClusterInput>| 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 = ClusterHandler::new(client);
let cluster = handler
.info()
.await
.tool_context("Failed to get cluster info")?;
CallToolResult::from_serialize(&cluster)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetLicenseInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_license(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_license")
.description(
"Get Redis Enterprise cluster license information including type, expiration date, \
cluster name, owner, and enabled features",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetLicenseInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetLicenseInput>| 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 = LicenseHandler::new(client);
let license = handler.get().await.tool_context("Failed to get license")?;
CallToolResult::from_serialize(&license)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetLicenseUsageInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_license_usage(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_license_usage")
.description(
"Get Redis Enterprise cluster license utilization statistics including shards, \
nodes, and RAM usage against license limits",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetLicenseUsageInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetLicenseUsageInput>| 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 = LicenseHandler::new(client);
let usage = handler
.usage()
.await
.tool_context("Failed to get license usage")?;
CallToolResult::from_serialize(&usage)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateLicenseInput {
#[serde(default)]
pub profile: Option<String>,
pub license_key: String,
}
pub fn update_license(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_license")
.description(
"Update the Redis Enterprise cluster license with a new license key. \
This applies a new license to the cluster. Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateLicenseInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<UpdateLicenseInput>| 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 = LicenseHandler::new(client);
let request = LicenseUpdateRequest {
license: input.license_key,
};
let license = handler
.update(request)
.await
.tool_context("Failed to update license")?;
CallToolResult::from_serialize(&license)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ValidateLicenseInput {
#[serde(default)]
pub profile: Option<String>,
pub license_key: String,
}
pub fn validate_license(state: Arc<AppState>) -> Tool {
ToolBuilder::new("validate_enterprise_license")
.description(
"Validate a license key before applying it to the Redis Enterprise cluster. \
Returns license information if valid, or an error if invalid. \
This is a dry-run that does not modify the cluster.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, ValidateLicenseInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<ValidateLicenseInput>| 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 = LicenseHandler::new(client);
let license = handler
.validate(&input.license_key)
.await
.tool_context("License validation failed")?;
CallToolResult::from_serialize(&license)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateClusterInput {
#[serde(default)]
pub profile: Option<String>,
pub updates: Value,
}
pub fn update_cluster(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_cluster")
.description(
"Update Redis Enterprise cluster configuration settings. \
Pass a JSON object with the fields to update (e.g., name, email_alerts, rack_aware). \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateClusterInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<UpdateClusterInput>| 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 = ClusterHandler::new(client);
let result = handler
.update(input.updates)
.await
.tool_context("Failed to update cluster")?;
CallToolResult::from_serialize(&result)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetClusterPolicyInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_cluster_policy(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_cluster_policy")
.description(
"Get Redis Enterprise cluster policy settings including default shards placement, \
rack awareness, default Redis version, and other cluster-wide defaults.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetClusterPolicyInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetClusterPolicyInput>| 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 = ClusterHandler::new(client);
let policy = handler
.policy()
.await
.tool_context("Failed to get cluster policy")?;
CallToolResult::from_serialize(&policy)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateClusterPolicyInput {
#[serde(default)]
pub profile: Option<String>,
pub policy: Value,
}
pub fn update_cluster_policy(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_cluster_policy")
.description(
"Update Redis Enterprise cluster policy settings. \
Common settings: default_shards_placement (dense/sparse), rack_aware, \
default_provisioned_redis_version, persistent_node_removal. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateClusterPolicyInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateClusterPolicyInput>| 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 = ClusterHandler::new(client);
let result = handler
.policy_update(input.policy)
.await
.tool_context("Failed to update cluster policy")?;
CallToolResult::from_serialize(&result)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct EnableMaintenanceModeInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn enable_maintenance_mode(state: Arc<AppState>) -> Tool {
ToolBuilder::new("enable_enterprise_maintenance_mode")
.description(
"Enable maintenance mode on the Redis Enterprise cluster. \
When enabled, cluster configuration changes are blocked, allowing safe \
maintenance operations like upgrades. Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, EnableMaintenanceModeInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<EnableMaintenanceModeInput>| 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 = ClusterHandler::new(client);
let result = handler
.update(serde_json::json!({"block_cluster_changes": true}))
.await
.tool_context("Failed to enable maintenance mode")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Maintenance mode enabled",
"result": result
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DisableMaintenanceModeInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn disable_maintenance_mode(state: Arc<AppState>) -> Tool {
ToolBuilder::new("disable_enterprise_maintenance_mode")
.description(
"Disable maintenance mode on the Redis Enterprise cluster. \
This re-enables cluster configuration changes after maintenance is complete. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, DisableMaintenanceModeInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<DisableMaintenanceModeInput>| 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 = ClusterHandler::new(client);
let result = handler
.update(serde_json::json!({"block_cluster_changes": false}))
.await
.tool_context("Failed to disable maintenance mode")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Maintenance mode disabled",
"result": result
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetClusterCertificatesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn get_cluster_certificates(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_enterprise_cluster_certificates")
.description(
"Get all certificates configured on the Redis Enterprise cluster including \
proxy certificates, syncer certificates, and API certificates.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetClusterCertificatesInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetClusterCertificatesInput>| 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 = ClusterHandler::new(client);
let certificates = handler
.certificates()
.await
.tool_context("Failed to get certificates")?;
CallToolResult::from_serialize(&certificates)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct RotateClusterCertificatesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn rotate_cluster_certificates(state: Arc<AppState>) -> Tool {
ToolBuilder::new("rotate_enterprise_cluster_certificates")
.description(
"Rotate all certificates on the Redis Enterprise cluster. \
This generates new certificates and replaces the existing ones. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, RotateClusterCertificatesInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<RotateClusterCertificatesInput>| 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 = ClusterHandler::new(client);
let result = handler
.certificates_rotate()
.await
.tool_context("Failed to rotate certificates")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Certificate rotation initiated",
"result": result
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateClusterCertificatesInput {
#[serde(default)]
pub profile: Option<String>,
pub name: String,
pub certificate: String,
pub key: String,
}
pub fn update_cluster_certificates(state: Arc<AppState>) -> Tool {
ToolBuilder::new("update_enterprise_cluster_certificates")
.description(
"Update a specific certificate on the Redis Enterprise cluster. \
Provide the certificate name (proxy, syncer, api), the PEM-encoded certificate, \
and the PEM-encoded private key. Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, UpdateClusterCertificatesInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<UpdateClusterCertificatesInput>| 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 = ClusterHandler::new(client);
let body = serde_json::json!({
"name": input.name,
"certificate": input.certificate,
"key": input.key
});
let result = handler
.update_cert(body)
.await
.tool_context("Failed to update certificate")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Certificate updated successfully",
"name": input.name,
"result": result
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListNodesInput {
#[serde(default)]
pub profile: Option<String>,
}
pub fn list_nodes(state: Arc<AppState>) -> Tool {
ToolBuilder::new("list_nodes")
.description("List all nodes in the Redis Enterprise cluster")
.read_only_safe()
.extractor_handler_typed::<_, _, _, ListNodesInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<ListNodesInput>| 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 = NodeHandler::new(client);
let nodes = handler.list().await.tool_context("Failed to list nodes")?;
crate::tools::wrap_list("nodes", &nodes)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetNodeInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn get_node(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_node")
.description(
"Get detailed information about a specific node in the Redis Enterprise cluster",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetNodeInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetNodeInput>| 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 = NodeHandler::new(client);
let node = handler
.get(input.uid)
.await
.tool_context("Failed to get node")?;
CallToolResult::from_serialize(&node)
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetNodeStatsInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
#[serde(default)]
pub interval: Option<String>,
#[serde(default)]
pub start_time: Option<String>,
#[serde(default)]
pub end_time: Option<String>,
}
pub fn get_node_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_node_stats")
.description(
"Get statistics for a specific node. By default returns the latest stats. \
Optionally specify interval and time range for historical data.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetNodeStatsInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<GetNodeStatsInput>| 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);
if input.interval.is_some()
|| input.start_time.is_some()
|| input.end_time.is_some()
{
let query = StatsQuery {
interval: input.interval,
stime: input.start_time,
etime: input.end_time,
metrics: None,
};
let stats = handler
.node(input.uid, Some(query))
.await
.tool_context("Failed to get node stats")?;
CallToolResult::from_serialize(&stats)
} else {
let stats = handler
.node_last(input.uid)
.await
.tool_context("Failed to get node stats")?;
CallToolResult::from_serialize(&stats)
}
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct NodeActionInput {
#[serde(default)]
pub profile: Option<String>,
pub uid: u32,
}
pub fn enable_node_maintenance(state: Arc<AppState>) -> Tool {
ToolBuilder::new("enable_enterprise_node_maintenance")
.description(
"Enable maintenance mode on a specific node in the Redis Enterprise cluster. \
Shards will be migrated off the node before maintenance begins. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, NodeActionInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<NodeActionInput>| 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 = NodeHandler::new(client);
let result = handler
.execute_action(input.uid, "maintenance_on")
.await
.tool_context("Failed to enable node maintenance")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Node maintenance mode enabled",
"node_uid": input.uid,
"action_uid": result.action_uid,
"description": result.description
}))
},
)
.build()
}
pub fn disable_node_maintenance(state: Arc<AppState>) -> Tool {
ToolBuilder::new("disable_enterprise_node_maintenance")
.description(
"Disable maintenance mode on a specific node in the Redis Enterprise cluster. \
The node will rejoin the cluster and accept shards again. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, NodeActionInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<NodeActionInput>| 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 = NodeHandler::new(client);
let result = handler
.execute_action(input.uid, "maintenance_off")
.await
.tool_context("Failed to disable node maintenance")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Node maintenance mode disabled",
"node_uid": input.uid,
"action_uid": result.action_uid,
"description": result.description
}))
},
)
.build()
}
pub fn rebalance_node(state: Arc<AppState>) -> Tool {
ToolBuilder::new("rebalance_enterprise_node")
.description(
"Rebalance shards on a specific node in the Redis Enterprise cluster. \
Redistributes shards across nodes for optimal performance. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, NodeActionInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<NodeActionInput>| 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 = NodeHandler::new(client);
let result = handler
.execute_action(input.uid, "rebalance")
.await
.tool_context("Failed to rebalance node")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Node rebalance initiated",
"node_uid": input.uid,
"action_uid": result.action_uid,
"description": result.description
}))
},
)
.build()
}
pub fn drain_node(state: Arc<AppState>) -> Tool {
ToolBuilder::new("drain_enterprise_node")
.description(
"Drain all shards from a specific node in the Redis Enterprise cluster. \
All shards will be migrated to other available nodes. \
Requires write permission.",
)
.non_destructive()
.extractor_handler_typed::<_, _, _, NodeActionInput>(
state,
|State(state): State<Arc<AppState>>, Json(input): Json<NodeActionInput>| 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 = NodeHandler::new(client);
let result = handler
.execute_action(input.uid, "drain")
.await
.tool_context("Failed to drain node")?;
CallToolResult::from_serialize(&serde_json::json!({
"message": "Node drain initiated",
"node_uid": input.uid,
"action_uid": result.action_uid,
"description": result.description
}))
},
)
.build()
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetClusterStatsInput {
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub interval: Option<String>,
#[serde(default)]
pub start_time: Option<String>,
#[serde(default)]
pub end_time: Option<String>,
}
pub fn get_cluster_stats(state: Arc<AppState>) -> Tool {
ToolBuilder::new("get_cluster_stats")
.description(
"Get statistics for the Redis Enterprise cluster. By default returns the latest \
stats. Optionally specify interval and time range for historical data.",
)
.read_only_safe()
.extractor_handler_typed::<_, _, _, GetClusterStatsInput>(
state,
|State(state): State<Arc<AppState>>,
Json(input): Json<GetClusterStatsInput>| 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);
if input.interval.is_some()
|| input.start_time.is_some()
|| input.end_time.is_some()
{
let query = StatsQuery {
interval: input.interval,
stime: input.start_time,
etime: input.end_time,
metrics: None,
};
let stats = handler
.cluster(Some(query))
.await
.tool_context("Failed to get cluster stats")?;
CallToolResult::from_serialize(&stats)
} else {
let stats = handler
.cluster_last()
.await
.tool_context("Failed to get cluster stats")?;
CallToolResult::from_serialize(&stats)
}
},
)
.build()
}
pub fn router(state: Arc<AppState>) -> McpRouter {
McpRouter::new()
.tool(get_cluster(state.clone()))
.tool(get_cluster_stats(state.clone()))
.tool(update_cluster(state.clone()))
.tool(get_cluster_policy(state.clone()))
.tool(update_cluster_policy(state.clone()))
.tool(enable_maintenance_mode(state.clone()))
.tool(disable_maintenance_mode(state.clone()))
.tool(get_cluster_certificates(state.clone()))
.tool(rotate_cluster_certificates(state.clone()))
.tool(update_cluster_certificates(state.clone()))
.tool(get_license(state.clone()))
.tool(get_license_usage(state.clone()))
.tool(update_license(state.clone()))
.tool(validate_license(state.clone()))
.tool(list_nodes(state.clone()))
.tool(get_node(state.clone()))
.tool(get_node_stats(state.clone()))
.tool(enable_node_maintenance(state.clone()))
.tool(disable_node_maintenance(state.clone()))
.tool(rebalance_node(state.clone()))
.tool(drain_node(state.clone()))
}