Skip to content

Commit e0263c1

Browse files
feat(mcp): add VPC Peering, Cloud Accounts, and CRDB Tasks tools (#560)
Add 9 new MCP tools: Cloud: - cloud_vpc_peerings_get: Get VPC peerings for a subscription - cloud_vpc_peering_delete: Delete a VPC peering - cloud_accounts_list: List all cloud accounts - cloud_account_get_by_id: Get a specific cloud account - cloud_account_delete: Delete a cloud account Enterprise: - enterprise_crdb_tasks_list: List all CRDB tasks - enterprise_crdb_task_get: Get a specific CRDB task - enterprise_crdb_tasks_by_crdb: List CRDB tasks by CRDB GUID - enterprise_crdb_task_cancel: Cancel a CRDB task
1 parent 42d2bae commit e0263c1

File tree

3 files changed

+278
-3
lines changed

3 files changed

+278
-3
lines changed

crates/redisctl-mcp/src/cloud_tools.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
55
use redis_cloud::fixed::subscriptions::FixedSubscriptionCreateRequest;
66
use redis_cloud::{
7-
AccountHandler, CloudClient, DatabaseHandler, FixedDatabaseHandler, FixedSubscriptionHandler,
8-
SubscriptionHandler, TaskHandler,
7+
AccountHandler, CloudAccountHandler, CloudClient, DatabaseHandler, FixedDatabaseHandler,
8+
FixedSubscriptionHandler, SubscriptionHandler, TaskHandler, VpcPeeringHandler,
99
};
1010
use redisctl_config::Config;
1111
use rmcp::{ErrorData as RmcpError, model::*};
@@ -322,4 +322,72 @@ impl CloudTools {
322322
.map_err(|e| self.to_error(e))?;
323323
self.to_result(serde_json::to_value(result).map_err(|e| self.to_error(e))?)
324324
}
325+
326+
// =========================================================================
327+
// VPC Peering Operations
328+
// =========================================================================
329+
330+
/// Get VPC peerings for a subscription
331+
pub async fn get_vpc_peerings(
332+
&self,
333+
subscription_id: i64,
334+
) -> Result<CallToolResult, RmcpError> {
335+
let handler = VpcPeeringHandler::new(self.client.clone());
336+
let peerings = handler
337+
.get(subscription_id as i32)
338+
.await
339+
.map_err(|e| self.to_error(e))?;
340+
self.to_result(serde_json::to_value(peerings).map_err(|e| self.to_error(e))?)
341+
}
342+
343+
/// Delete a VPC peering
344+
pub async fn delete_vpc_peering(
345+
&self,
346+
subscription_id: i64,
347+
peering_id: i64,
348+
) -> Result<CallToolResult, RmcpError> {
349+
let handler = VpcPeeringHandler::new(self.client.clone());
350+
handler
351+
.delete(subscription_id as i32, peering_id as i32)
352+
.await
353+
.map_err(|e| self.to_error(e))?;
354+
self.to_result(serde_json::json!({
355+
"success": true,
356+
"message": format!("VPC peering {} deleted from subscription {}", peering_id, subscription_id)
357+
}))
358+
}
359+
360+
// =========================================================================
361+
// Cloud Account Operations
362+
// =========================================================================
363+
364+
/// List all cloud accounts
365+
pub async fn list_cloud_accounts(&self) -> Result<CallToolResult, RmcpError> {
366+
let handler = CloudAccountHandler::new(self.client.clone());
367+
let accounts = handler
368+
.get_cloud_accounts()
369+
.await
370+
.map_err(|e| self.to_error(e))?;
371+
self.to_result(serde_json::to_value(accounts).map_err(|e| self.to_error(e))?)
372+
}
373+
374+
/// Get a specific cloud account
375+
pub async fn get_cloud_account(&self, account_id: i64) -> Result<CallToolResult, RmcpError> {
376+
let handler = CloudAccountHandler::new(self.client.clone());
377+
let account = handler
378+
.get_cloud_account_by_id(account_id as i32)
379+
.await
380+
.map_err(|e| self.to_error(e))?;
381+
self.to_result(serde_json::to_value(account).map_err(|e| self.to_error(e))?)
382+
}
383+
384+
/// Delete a cloud account
385+
pub async fn delete_cloud_account(&self, account_id: i64) -> Result<CallToolResult, RmcpError> {
386+
let handler = CloudAccountHandler::new(self.client.clone());
387+
let result = handler
388+
.delete_cloud_account(account_id as i32)
389+
.await
390+
.map_err(|e| self.to_error(e))?;
391+
self.to_result(serde_json::to_value(result).map_err(|e| self.to_error(e))?)
392+
}
325393
}

crates/redisctl-mcp/src/enterprise_tools.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Wraps Redis Enterprise API client operations for MCP tool invocation.
44
55
use redis_enterprise::{
6-
AlertHandler, BdbHandler, ClusterHandler, CrdbHandler, CreateDatabaseRequest,
6+
AlertHandler, BdbHandler, ClusterHandler, CrdbHandler, CrdbTasksHandler, CreateDatabaseRequest,
77
CreateLdapMappingRequest, CreateRedisAclRequest, CreateRoleRequest, CreateUserRequest,
88
DebugInfoHandler, DiagnosticRequest, DiagnosticsHandler, EndpointsHandler, EnterpriseClient,
99
JobSchedulerHandler, LdapMappingHandler, LicenseHandler, LogsHandler, ModuleHandler,
@@ -853,4 +853,48 @@ impl EnterpriseTools {
853853
let report = handler.run(request).await.map_err(|e| self.to_error(e))?;
854854
self.to_result(serde_json::to_value(report).map_err(|e| self.to_error(e))?)
855855
}
856+
857+
// =========================================================================
858+
// CRDB Task Operations
859+
// =========================================================================
860+
861+
/// List all CRDB tasks
862+
pub async fn list_crdb_tasks(&self) -> Result<CallToolResult, RmcpError> {
863+
let handler = CrdbTasksHandler::new(self.client.clone());
864+
let tasks = handler.list().await.map_err(|e| self.to_error(e))?;
865+
self.to_result(serde_json::to_value(tasks).map_err(|e| self.to_error(e))?)
866+
}
867+
868+
/// Get a specific CRDB task
869+
pub async fn get_crdb_task(&self, task_id: &str) -> Result<CallToolResult, RmcpError> {
870+
let handler = CrdbTasksHandler::new(self.client.clone());
871+
let task = handler.get(task_id).await.map_err(|e| self.to_error(e))?;
872+
self.to_result(serde_json::to_value(task).map_err(|e| self.to_error(e))?)
873+
}
874+
875+
/// List CRDB tasks for a specific Active-Active database
876+
pub async fn list_crdb_tasks_by_crdb(
877+
&self,
878+
crdb_guid: &str,
879+
) -> Result<CallToolResult, RmcpError> {
880+
let handler = CrdbTasksHandler::new(self.client.clone());
881+
let tasks = handler
882+
.list_by_crdb(crdb_guid)
883+
.await
884+
.map_err(|e| self.to_error(e))?;
885+
self.to_result(serde_json::to_value(tasks).map_err(|e| self.to_error(e))?)
886+
}
887+
888+
/// Cancel a CRDB task
889+
pub async fn cancel_crdb_task(&self, task_id: &str) -> Result<CallToolResult, RmcpError> {
890+
let handler = CrdbTasksHandler::new(self.client.clone());
891+
handler
892+
.cancel(task_id)
893+
.await
894+
.map_err(|e| self.to_error(e))?;
895+
self.to_result(serde_json::json!({
896+
"success": true,
897+
"message": format!("CRDB task {} cancelled", task_id)
898+
}))
899+
}
856900
}

crates/redisctl-mcp/src/server.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,32 @@ pub struct EssentialsDatabaseIdParam {
337337
pub database_id: i64,
338338
}
339339

340+
// Cloud - VPC Peering parameter structs
341+
342+
#[derive(Debug, Deserialize, schemars::JsonSchema)]
343+
pub struct VpcPeeringIdParam {
344+
/// The subscription ID
345+
pub subscription_id: i64,
346+
/// The VPC peering ID
347+
pub peering_id: i64,
348+
}
349+
350+
// Cloud - Cloud Account parameter structs
351+
352+
#[derive(Debug, Deserialize, schemars::JsonSchema)]
353+
pub struct CloudAccountIdParam {
354+
/// The cloud account ID
355+
pub account_id: i64,
356+
}
357+
358+
// Enterprise - CRDB Task parameter structs
359+
360+
#[derive(Debug, Deserialize, schemars::JsonSchema)]
361+
pub struct CrdbTaskIdParam {
362+
/// The CRDB task ID
363+
pub task_id: String,
364+
}
365+
340366
impl RedisCtlMcp {
341367
/// Create a new MCP server instance
342368
pub fn new(profile: Option<&str>, read_only: bool) -> anyhow::Result<Self> {
@@ -1809,6 +1835,143 @@ impl RedisCtlMcp {
18091835
.delete_essentials_database(params.subscription_id, params.database_id)
18101836
.await
18111837
}
1838+
1839+
// =========================================================================
1840+
// Cloud Tools - VPC Peering Operations
1841+
// =========================================================================
1842+
1843+
#[tool(description = "Get VPC peerings for a subscription")]
1844+
async fn cloud_vpc_peerings_get(
1845+
&self,
1846+
Parameters(params): Parameters<SubscriptionIdParam>,
1847+
) -> Result<CallToolResult, RmcpError> {
1848+
info!(
1849+
subscription_id = params.subscription_id,
1850+
"Tool called: cloud_vpc_peerings_get"
1851+
);
1852+
let tools = self.get_cloud_tools().await?;
1853+
tools.get_vpc_peerings(params.subscription_id).await
1854+
}
1855+
1856+
#[tool(description = "Delete a VPC peering. This is a destructive operation.")]
1857+
async fn cloud_vpc_peering_delete(
1858+
&self,
1859+
Parameters(params): Parameters<VpcPeeringIdParam>,
1860+
) -> Result<CallToolResult, RmcpError> {
1861+
info!(
1862+
subscription_id = params.subscription_id,
1863+
peering_id = params.peering_id,
1864+
"Tool called: cloud_vpc_peering_delete"
1865+
);
1866+
1867+
if self.config.read_only {
1868+
return Err(RmcpError::invalid_request(
1869+
"Server is in read-only mode. Use --allow-writes to enable write operations.",
1870+
None,
1871+
));
1872+
}
1873+
1874+
let tools = self.get_cloud_tools().await?;
1875+
tools
1876+
.delete_vpc_peering(params.subscription_id, params.peering_id)
1877+
.await
1878+
}
1879+
1880+
// =========================================================================
1881+
// Cloud Tools - Cloud Account Operations
1882+
// =========================================================================
1883+
1884+
#[tool(
1885+
description = "List all cloud provider accounts (AWS, GCP, Azure) configured in your Redis Cloud account"
1886+
)]
1887+
async fn cloud_accounts_list(&self) -> Result<CallToolResult, RmcpError> {
1888+
info!("Tool called: cloud_accounts_list");
1889+
let tools = self.get_cloud_tools().await?;
1890+
tools.list_cloud_accounts().await
1891+
}
1892+
1893+
#[tool(description = "Get detailed information about a specific cloud provider account")]
1894+
async fn cloud_account_get_by_id(
1895+
&self,
1896+
Parameters(params): Parameters<CloudAccountIdParam>,
1897+
) -> Result<CallToolResult, RmcpError> {
1898+
info!(
1899+
account_id = params.account_id,
1900+
"Tool called: cloud_account_get_by_id"
1901+
);
1902+
let tools = self.get_cloud_tools().await?;
1903+
tools.get_cloud_account(params.account_id).await
1904+
}
1905+
1906+
#[tool(description = "Delete a cloud provider account. This is a destructive operation.")]
1907+
async fn cloud_account_delete(
1908+
&self,
1909+
Parameters(params): Parameters<CloudAccountIdParam>,
1910+
) -> Result<CallToolResult, RmcpError> {
1911+
info!(
1912+
account_id = params.account_id,
1913+
"Tool called: cloud_account_delete"
1914+
);
1915+
1916+
if self.config.read_only {
1917+
return Err(RmcpError::invalid_request(
1918+
"Server is in read-only mode. Use --allow-writes to enable write operations.",
1919+
None,
1920+
));
1921+
}
1922+
1923+
let tools = self.get_cloud_tools().await?;
1924+
tools.delete_cloud_account(params.account_id).await
1925+
}
1926+
1927+
// =========================================================================
1928+
// Enterprise Tools - CRDB Task Operations
1929+
// =========================================================================
1930+
1931+
#[tool(description = "List all Active-Active (CRDB) tasks in the Redis Enterprise cluster")]
1932+
async fn enterprise_crdb_tasks_list(&self) -> Result<CallToolResult, RmcpError> {
1933+
info!("Tool called: enterprise_crdb_tasks_list");
1934+
let tools = self.get_enterprise_tools().await?;
1935+
tools.list_crdb_tasks().await
1936+
}
1937+
1938+
#[tool(description = "Get detailed information about a specific CRDB task")]
1939+
async fn enterprise_crdb_task_get(
1940+
&self,
1941+
Parameters(params): Parameters<CrdbTaskIdParam>,
1942+
) -> Result<CallToolResult, RmcpError> {
1943+
info!(task_id = %params.task_id, "Tool called: enterprise_crdb_task_get");
1944+
let tools = self.get_enterprise_tools().await?;
1945+
tools.get_crdb_task(&params.task_id).await
1946+
}
1947+
1948+
#[tool(description = "List CRDB tasks for a specific Active-Active database")]
1949+
async fn enterprise_crdb_tasks_by_crdb(
1950+
&self,
1951+
Parameters(params): Parameters<CrdbGuidParam>,
1952+
) -> Result<CallToolResult, RmcpError> {
1953+
info!(crdb_guid = %params.crdb_guid, "Tool called: enterprise_crdb_tasks_by_crdb");
1954+
let tools = self.get_enterprise_tools().await?;
1955+
tools.list_crdb_tasks_by_crdb(&params.crdb_guid).await
1956+
}
1957+
1958+
#[tool(description = "Cancel a CRDB task")]
1959+
async fn enterprise_crdb_task_cancel(
1960+
&self,
1961+
Parameters(params): Parameters<CrdbTaskIdParam>,
1962+
) -> Result<CallToolResult, RmcpError> {
1963+
info!(task_id = %params.task_id, "Tool called: enterprise_crdb_task_cancel");
1964+
1965+
if self.config.read_only {
1966+
return Err(RmcpError::invalid_request(
1967+
"Server is in read-only mode. Use --allow-writes to enable write operations.",
1968+
None,
1969+
));
1970+
}
1971+
1972+
let tools = self.get_enterprise_tools().await?;
1973+
tools.cancel_crdb_task(&params.task_id).await
1974+
}
18121975
}
18131976

18141977
#[tool_handler]

0 commit comments

Comments
 (0)