Skip to main content

redisctl_mcp/
lib.rs

1//! MCP (Model Context Protocol) server for Redis Cloud and Enterprise
2//!
3//! This crate provides an MCP server that exposes Redis Cloud and Enterprise
4//! management operations as tools for AI systems.
5//!
6//! ## Binary Usage
7//!
8//! The primary way to use this crate is as a standalone binary:
9//!
10//! ```bash
11//! # Stdio transport (for Claude Desktop, etc.)
12//! redisctl-mcp --profile my-profile
13//!
14//! # Multiple profiles for multi-cluster support
15//! redisctl-mcp --profile cluster-west --profile cluster-east --profile cluster-central
16//!
17//! # HTTP transport with OAuth (for shared deployments)
18//! redisctl-mcp --transport http --port 8080 --oauth --oauth-issuer https://accounts.google.com
19//!
20//! # Enable only specific toolsets
21//! redisctl-mcp --tools cloud,app
22//! ```
23//!
24//! ## Library Usage
25//!
26//! You can also embed the tools in your own MCP server using sub-routers:
27//!
28//! ```no_run
29//! use std::sync::Arc;
30//! use redisctl_mcp::{AppState, CredentialSource, tools};
31//! use redisctl_mcp::policy::{Policy, PolicyConfig};
32//! use tower_mcp::McpRouter;
33//!
34//! # async fn example() -> anyhow::Result<()> {
35//! let policy = Arc::new(Policy::new(
36//!     PolicyConfig::default(), // read-only
37//!     std::collections::HashMap::new(),
38//!     "default".to_string(),
39//! ));
40//! let state = Arc::new(AppState::new(
41//!     CredentialSource::Profiles(vec!["default".to_string()]),
42//!     policy,
43//!     None, // no database URL
44//! )?);
45//!
46//! // Use merge to compose sub-routers
47//! let router = McpRouter::new()
48//!     .merge(tools::cloud::router(state.clone()))
49//!     .merge(tools::enterprise::router(state.clone()))
50//!     .merge(tools::profile::router(state.clone()));
51//! # Ok(())
52//! # }
53//! ```
54
55pub mod audit;
56pub mod error;
57pub mod policy;
58pub mod presets;
59pub mod prompts;
60pub mod resources;
61pub mod serde_helpers;
62pub mod state;
63pub mod tools;
64
65pub use error::McpError;
66pub use state::{AppState, CredentialSource};
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::sync::Arc;
72
73    #[test]
74    fn test_credential_source_profiles() {
75        let source = CredentialSource::Profiles(vec!["test".to_string()]);
76        match source {
77            CredentialSource::Profiles(profiles) => assert_eq!(profiles, vec!["test".to_string()]),
78            _ => panic!("Expected Profiles variant"),
79        }
80    }
81
82    #[test]
83    fn test_credential_source_multiple_profiles() {
84        let source = CredentialSource::Profiles(vec![
85            "cluster-west".to_string(),
86            "cluster-east".to_string(),
87        ]);
88        match source {
89            CredentialSource::Profiles(profiles) => {
90                assert_eq!(profiles.len(), 2);
91                assert_eq!(profiles[0], "cluster-west");
92                assert_eq!(profiles[1], "cluster-east");
93            }
94            _ => panic!("Expected Profiles variant"),
95        }
96    }
97
98    #[test]
99    fn test_credential_source_oauth() {
100        let source = CredentialSource::OAuth {
101            issuer: Some("https://example.com".to_string()),
102            audience: Some("my-api".to_string()),
103        };
104        match source {
105            CredentialSource::OAuth { issuer, audience } => {
106                assert_eq!(issuer, Some("https://example.com".to_string()));
107                assert_eq!(audience, Some("my-api".to_string()));
108            }
109            _ => panic!("Expected OAuth variant"),
110        }
111    }
112
113    #[test]
114    fn test_app_state_read_only() {
115        use policy::{Policy, PolicyConfig};
116        let read_only_policy = Arc::new(Policy::new(
117            PolicyConfig::default(), // read-only
118            std::collections::HashMap::new(),
119            "test".to_string(),
120        ));
121        let state =
122            AppState::new(CredentialSource::Profiles(vec![]), read_only_policy, None).unwrap();
123
124        assert!(!state.is_write_allowed());
125    }
126
127    #[test]
128    fn test_app_state_write_allowed() {
129        use policy::{Policy, PolicyConfig, SafetyTier};
130        let write_policy = Arc::new(Policy::new(
131            PolicyConfig {
132                tier: SafetyTier::Full,
133                ..Default::default()
134            },
135            std::collections::HashMap::new(),
136            "test".to_string(),
137        ));
138        let state = AppState::new(CredentialSource::Profiles(vec![]), write_policy, None).unwrap();
139
140        assert!(state.is_write_allowed());
141    }
142
143    #[test]
144    fn test_app_state_database_url() {
145        let state = AppState::new(
146            CredentialSource::Profiles(vec![]),
147            AppState::test_policy(),
148            Some("redis://localhost:6379".to_string()),
149        )
150        .unwrap();
151
152        assert_eq!(
153            state.database_url,
154            Some("redis://localhost:6379".to_string())
155        );
156    }
157
158    #[test]
159    fn test_app_state_available_profiles() {
160        let state = AppState::new(
161            CredentialSource::Profiles(vec![
162                "cluster-west".to_string(),
163                "cluster-east".to_string(),
164            ]),
165            AppState::test_policy(),
166            None,
167        )
168        .unwrap();
169
170        let profiles = state.available_profiles();
171        assert_eq!(profiles.len(), 2);
172        assert_eq!(profiles[0], "cluster-west");
173        assert_eq!(profiles[1], "cluster-east");
174    }
175
176    #[cfg(feature = "cloud")]
177    #[test]
178    fn test_cloud_tools_build() {
179        let state = Arc::new(
180            AppState::new(
181                CredentialSource::Profiles(vec![]),
182                AppState::test_policy(),
183                None,
184            )
185            .unwrap(),
186        );
187
188        // Verify all cloud tools build successfully
189        // Subscriptions & Databases
190        let _ = tools::cloud::list_subscriptions(state.clone());
191        let _ = tools::cloud::get_subscription(state.clone());
192        let _ = tools::cloud::list_databases(state.clone());
193        let _ = tools::cloud::get_database(state.clone());
194        let _ = tools::cloud::get_backup_status(state.clone());
195        let _ = tools::cloud::get_slow_log(state.clone());
196        let _ = tools::cloud::get_tags(state.clone());
197        // Account & Configuration
198        let _ = tools::cloud::get_account(state.clone());
199        let _ = tools::cloud::get_regions(state.clone());
200        let _ = tools::cloud::get_modules(state.clone());
201        let _ = tools::cloud::list_account_users(state.clone());
202        let _ = tools::cloud::get_account_user(state.clone());
203        let _ = tools::cloud::list_acl_users(state.clone());
204        let _ = tools::cloud::get_acl_user(state.clone());
205        let _ = tools::cloud::list_acl_roles(state.clone());
206        let _ = tools::cloud::list_redis_rules(state.clone());
207        // Logs
208        let _ = tools::cloud::get_system_logs(state.clone());
209        let _ = tools::cloud::get_session_logs(state.clone());
210        // Tasks
211        let _ = tools::cloud::list_tasks(state.clone());
212        let _ = tools::cloud::get_task(state.clone());
213        // Write operations
214        let _ = tools::cloud::create_database(state.clone());
215        let _ = tools::cloud::update_database(state.clone());
216        let _ = tools::cloud::delete_database(state.clone());
217        let _ = tools::cloud::backup_database(state.clone());
218        let _ = tools::cloud::import_database(state.clone());
219        let _ = tools::cloud::delete_subscription(state.clone());
220        let _ = tools::cloud::flush_database(state.clone());
221        let _ = tools::cloud::create_subscription(state.clone());
222        // Raw
223        let _ = tools::cloud::cloud_raw_api(state.clone());
224    }
225
226    #[cfg(feature = "enterprise")]
227    #[test]
228    fn test_enterprise_tools_build() {
229        let state = Arc::new(
230            AppState::new(
231                CredentialSource::Profiles(vec![]),
232                AppState::test_policy(),
233                None,
234            )
235            .unwrap(),
236        );
237
238        // Verify all enterprise tools build successfully
239        // Cluster
240        let _ = tools::enterprise::get_cluster(state.clone());
241        // License
242        let _ = tools::enterprise::get_license(state.clone());
243        let _ = tools::enterprise::get_license_usage(state.clone());
244        // Logs
245        let _ = tools::enterprise::list_logs(state.clone());
246        // Databases
247        let _ = tools::enterprise::list_databases(state.clone());
248        let _ = tools::enterprise::get_database(state.clone());
249        // Nodes
250        let _ = tools::enterprise::list_nodes(state.clone());
251        let _ = tools::enterprise::get_node(state.clone());
252        // Users
253        let _ = tools::enterprise::list_users(state.clone());
254        let _ = tools::enterprise::get_user(state.clone());
255        // Alerts
256        let _ = tools::enterprise::list_alerts(state.clone());
257        let _ = tools::enterprise::list_database_alerts(state.clone());
258        // Stats
259        let _ = tools::enterprise::get_cluster_stats(state.clone());
260        let _ = tools::enterprise::get_database_stats(state.clone());
261        let _ = tools::enterprise::get_node_stats(state.clone());
262        let _ = tools::enterprise::get_all_nodes_stats(state.clone());
263        let _ = tools::enterprise::get_all_databases_stats(state.clone());
264        // Shards
265        let _ = tools::enterprise::list_shards(state.clone());
266        let _ = tools::enterprise::get_shard(state.clone());
267        let _ = tools::enterprise::get_shard_stats(state.clone());
268        let _ = tools::enterprise::get_all_shards_stats(state.clone());
269        // Endpoints
270        let _ = tools::enterprise::get_database_endpoints(state.clone());
271        // Debug info
272        let _ = tools::enterprise::list_debug_info_tasks(state.clone());
273        let _ = tools::enterprise::get_debug_info_status(state.clone());
274        // Modules
275        let _ = tools::enterprise::list_modules(state.clone());
276        let _ = tools::enterprise::get_module(state.clone());
277        // Write operations
278        let _ = tools::enterprise::backup_enterprise_database(state.clone());
279        let _ = tools::enterprise::import_enterprise_database(state.clone());
280        let _ = tools::enterprise::create_enterprise_database(state.clone());
281        let _ = tools::enterprise::update_enterprise_database(state.clone());
282        let _ = tools::enterprise::delete_enterprise_database(state.clone());
283        let _ = tools::enterprise::flush_enterprise_database(state.clone());
284        // Raw
285        let _ = tools::enterprise::enterprise_raw_api(state.clone());
286    }
287
288    #[cfg(feature = "database")]
289    #[test]
290    fn test_database_tools_build() {
291        let state = Arc::new(
292            AppState::new(
293                CredentialSource::Profiles(vec![]),
294                AppState::test_policy(),
295                Some("redis://localhost:6379".to_string()),
296            )
297            .unwrap(),
298        );
299
300        // Verify database tools build successfully
301        let _ = tools::redis::ping(state.clone());
302        let _ = tools::redis::info(state.clone());
303        let _ = tools::redis::keys(state.clone());
304        let _ = tools::redis::get(state.clone());
305        let _ = tools::redis::set(state.clone());
306        let _ = tools::redis::del(state.clone());
307        let _ = tools::redis::flushdb(state.clone());
308        // Raw
309        let _ = tools::redis::redis_command(state.clone());
310    }
311
312    #[test]
313    fn test_profile_tools_build() {
314        let state = Arc::new(
315            AppState::new(
316                CredentialSource::Profiles(vec![]),
317                AppState::test_policy(),
318                None,
319            )
320            .unwrap(),
321        );
322
323        // Verify profile tools build successfully
324        let _ = tools::profile::list_profiles(state.clone());
325        let _ = tools::profile::show_profile(state.clone());
326        let _ = tools::profile::config_path(state.clone());
327        let _ = tools::profile::validate_config(state.clone());
328        let _ = tools::profile::set_default_cloud(state.clone());
329        let _ = tools::profile::set_default_enterprise(state.clone());
330        let _ = tools::profile::delete_profile(state.clone());
331    }
332}