Skip to content

Commit 7ab28d4

Browse files
committed
feat(mcp): add --database-url CLI option for direct Redis connections
Add support for connecting to Redis databases via URL without requiring a profile. This is useful for ad-hoc connections to standalone Redis instances that aren't configured as profiles. Changes: - Add --database-url option to 'mcp serve' command (supports REDIS_URL env) - Add DatabaseTools::new_from_url() constructor for URL-based connections - Update get_database_tools() to prefer URL over profile when both available - Update MCP documentation with both connection approaches - Add unit test for database_url configuration The URL format follows the standard Redis URL scheme: redis[s]://[[username:]password@]host[:port][/db]
1 parent 160a945 commit 7ab28d4

6 files changed

Lines changed: 138 additions & 15 deletions

File tree

crates/redisctl-mcp/src/database_tools.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ impl DatabaseTools {
6363
Ok(Self { conn })
6464
}
6565

66+
/// Create a new DatabaseTools instance from a direct Redis URL.
67+
///
68+
/// URL format: redis[s]://[[username:]password@]host[:port][/db]
69+
///
70+
/// This is useful for ad-hoc connections to Redis databases that aren't
71+
/// configured as profiles, such as standalone Redis instances.
72+
pub async fn new_from_url(url: &str) -> anyhow::Result<Self> {
73+
debug!(url = %url.split('@').next_back().unwrap_or("[redacted]"), "Connecting to Redis via URL");
74+
75+
let client = redis::Client::open(url)?;
76+
let conn = ConnectionManager::new(client).await?;
77+
78+
Ok(Self { conn })
79+
}
80+
6681
/// Execute an arbitrary Redis command.
6782
///
6883
/// This is the generic execute function that can run any Redis command.

crates/redisctl-mcp/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
//!
1212
//! #[tokio::main]
1313
//! async fn main() -> anyhow::Result<()> {
14-
//! let server = RedisCtlMcp::new(None, true)?; // profile=None, read_only=true
14+
//! // profile=None, read_only=true, database_url=None
15+
//! let server = RedisCtlMcp::new(None, true, None)?;
1516
//! let service = server.serve(stdio()).await?;
1617
//! service.waiting().await?;
1718
//! Ok(())
@@ -28,17 +29,22 @@ pub use error::McpError;
2829
pub use server::RedisCtlMcp;
2930

3031
/// Start the MCP server with stdio transport
31-
pub async fn serve_stdio(profile: Option<&str>, read_only: bool) -> anyhow::Result<()> {
32+
pub async fn serve_stdio(
33+
profile: Option<&str>,
34+
read_only: bool,
35+
database_url: Option<&str>,
36+
) -> anyhow::Result<()> {
3237
use rmcp::{ServiceExt, transport::stdio};
3338
use tracing::info;
3439

3540
info!(
3641
profile = profile,
3742
read_only = read_only,
43+
database_url = database_url.map(|_| "[redacted]"),
3844
"Starting MCP server"
3945
);
4046

41-
let server = RedisCtlMcp::new(profile, read_only)?;
47+
let server = RedisCtlMcp::new(profile, read_only, database_url)?;
4248
let service = server.serve(stdio()).await?;
4349
service.waiting().await?;
4450

crates/redisctl-mcp/src/server.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub struct ServerConfig {
2222
pub profile: Option<String>,
2323
/// Whether the server is in read-only mode
2424
pub read_only: bool,
25+
/// Optional database URL for direct connections (overrides profile)
26+
pub database_url: Option<String>,
2527
}
2628

2729
/// MCP server for Redis Cloud and Enterprise management
@@ -1873,15 +1875,21 @@ pub struct PubsubNumsubParam {
18731875

18741876
impl RedisCtlMcp {
18751877
/// Create a new MCP server instance
1876-
pub fn new(profile: Option<&str>, read_only: bool) -> anyhow::Result<Self> {
1878+
pub fn new(
1879+
profile: Option<&str>,
1880+
read_only: bool,
1881+
database_url: Option<&str>,
1882+
) -> anyhow::Result<Self> {
18771883
let config = Arc::new(ServerConfig {
18781884
profile: profile.map(String::from),
18791885
read_only,
1886+
database_url: database_url.map(String::from),
18801887
});
18811888

18821889
info!(
18831890
profile = ?config.profile,
18841891
read_only = config.read_only,
1892+
database_url = config.database_url.as_ref().map(|_| "[configured]"),
18851893
"Initializing RedisCtlMcp server"
18861894
);
18871895

@@ -1924,13 +1932,24 @@ impl RedisCtlMcp {
19241932
}
19251933

19261934
/// Initialize Database tools lazily
1935+
///
1936+
/// If `database_url` is configured, connects directly using that URL.
1937+
/// Otherwise, uses the profile-based connection (falls back to default database profile).
19271938
async fn get_database_tools(&self) -> Result<DatabaseTools, RmcpError> {
19281939
let mut guard = self.database_tools.write().await;
19291940
if guard.is_none() {
19301941
debug!("Initializing Database tools");
1931-
let tools = DatabaseTools::new(self.config.profile.as_deref())
1932-
.await
1933-
.map_err(|e| RmcpError::internal_error(e.to_string(), None))?;
1942+
let tools = if let Some(ref url) = self.config.database_url {
1943+
debug!("Using direct database URL connection");
1944+
DatabaseTools::new_from_url(url)
1945+
.await
1946+
.map_err(|e| RmcpError::internal_error(e.to_string(), None))?
1947+
} else {
1948+
debug!("Using profile-based database connection");
1949+
DatabaseTools::new(self.config.profile.as_deref())
1950+
.await
1951+
.map_err(|e| RmcpError::internal_error(e.to_string(), None))?
1952+
};
19341953
*guard = Some(tools);
19351954
}
19361955
Ok(guard.clone().unwrap())
@@ -7450,19 +7469,34 @@ mod tests {
74507469

74517470
#[test]
74527471
fn test_server_creation() {
7453-
let server = RedisCtlMcp::new(None, true);
7472+
let server = RedisCtlMcp::new(None, true, None);
74547473
assert!(server.is_ok());
74557474
let server = server.unwrap();
74567475
assert!(server.config().read_only);
74577476
assert!(server.config().profile.is_none());
7477+
assert!(server.config().database_url.is_none());
74587478
}
74597479

74607480
#[test]
74617481
fn test_server_with_profile() {
7462-
let server = RedisCtlMcp::new(Some("test-profile"), false);
7482+
let server = RedisCtlMcp::new(Some("test-profile"), false, None);
74637483
assert!(server.is_ok());
74647484
let server = server.unwrap();
74657485
assert!(!server.config().read_only);
74667486
assert_eq!(server.config().profile, Some("test-profile".to_string()));
7487+
assert!(server.config().database_url.is_none());
7488+
}
7489+
7490+
#[test]
7491+
fn test_server_with_database_url() {
7492+
let server = RedisCtlMcp::new(None, true, Some("redis://localhost:6379"));
7493+
assert!(server.is_ok());
7494+
let server = server.unwrap();
7495+
assert!(server.config().read_only);
7496+
assert!(server.config().profile.is_none());
7497+
assert_eq!(
7498+
server.config().database_url,
7499+
Some("redis://localhost:6379".to_string())
7500+
);
74677501
}
74687502
}

crates/redisctl/src/cli/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,23 @@ pub enum McpCommands {
216216
217217
# Enable write operations (destructive operations allowed)
218218
redisctl mcp serve --allow-writes
219+
220+
# Connect to a specific Redis database
221+
redisctl mcp serve --database-url redis://localhost:6379
222+
223+
# Full access with database connection
224+
redisctl mcp serve --allow-writes --database-url redis://:password@localhost:6379
219225
")]
220226
Serve {
221227
/// Allow write operations (create, update, delete). Default is read-only.
222228
#[arg(long)]
223229
allow_writes: bool,
230+
231+
/// Redis database URL for direct database operations.
232+
/// Format: redis[s]://[[username:]password@]host[:port][/db]
233+
/// If not specified, uses the default database profile from config.
234+
#[arg(long, env = "REDIS_URL")]
235+
database_url: Option<String>,
224236
},
225237

226238
/// List available MCP tools

crates/redisctl/src/main.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,14 @@ fn format_command(command: &Commands) -> String {
225225
Commands::Mcp(cmd) => {
226226
use cli::McpCommands::*;
227227
match cmd {
228-
Serve { allow_writes } => format!("mcp serve (allow_writes={})", allow_writes),
228+
Serve {
229+
allow_writes,
230+
database_url,
231+
} => format!(
232+
"mcp serve (allow_writes={}, database_url={:?})",
233+
allow_writes,
234+
database_url.as_ref().map(|_| "[redacted]")
235+
),
229236
Tools => "mcp tools".to_string(),
230237
}
231238
}
@@ -237,10 +244,17 @@ async fn execute_mcp_command(cli: &Cli, mcp_cmd: &cli::McpCommands) -> Result<()
237244
use cli::McpCommands::*;
238245

239246
match mcp_cmd {
240-
Serve { allow_writes } => {
247+
Serve {
248+
allow_writes,
249+
database_url,
250+
} => {
241251
let read_only = !allow_writes;
242-
debug!("Starting MCP server (read_only={})", read_only);
243-
redisctl_mcp::serve_stdio(cli.profile.as_deref(), read_only)
252+
debug!(
253+
"Starting MCP server (read_only={}, database_url={:?})",
254+
read_only,
255+
database_url.as_ref().map(|_| "[redacted]")
256+
);
257+
redisctl_mcp::serve_stdio(cli.profile.as_deref(), read_only, database_url.as_deref())
244258
.await
245259
.map_err(|e| RedisCtlError::Configuration(e.to_string()))
246260
}

mkdocs-site/docs/mcp/getting-started.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ redisctl mcp tools
8383

8484
### Database Connection Options
8585

86-
The `--database-url` flag enables 125+ database tools for direct Redis operations including all data types, Redis Stack modules (Search, JSON, TimeSeries, Bloom), Streams, and Pub/Sub.
86+
The MCP server provides 125+ database tools for direct Redis operations including all data types, Redis Stack modules (Search, JSON, TimeSeries, Bloom), Streams, and Pub/Sub. You can connect in two ways:
87+
88+
#### Option 1: Direct URL (Recommended for Ad-Hoc Connections)
89+
90+
Use `--database-url` for quick connections to any Redis database:
8791

8892
```bash
8993
# Local Redis
@@ -92,13 +96,51 @@ The `--database-url` flag enables 125+ database tools for direct Redis operation
9296
# With password
9397
--database-url redis://:mypassword@localhost:6379
9498

99+
# With username and password
100+
--database-url redis://myuser:mypassword@localhost:6379
101+
95102
# Redis Cloud/Enterprise database
96103
--database-url redis://default:password@redis-12345.cloud.redislabs.com:12345
97104

98-
# TLS connection
105+
# TLS connection (use rediss:// scheme)
99106
--database-url rediss://default:password@redis-12345.cloud.redislabs.com:12345
107+
108+
# Using environment variable
109+
REDIS_URL=redis://localhost:6379 redisctl mcp serve
110+
```
111+
112+
#### Option 2: Database Profile (Recommended for Regular Use)
113+
114+
Configure a database profile in your redisctl config file (`~/.config/redisctl/config.toml` or `~/Library/Application Support/redisctl/config.toml` on macOS):
115+
116+
```toml
117+
# Default database profile to use when none specified
118+
default_database_profile = "local-redis"
119+
120+
[profiles.local-redis]
121+
deployment_type = "database"
122+
123+
[profiles.local-redis.credentials.database]
124+
host = "localhost"
125+
port = 6379
126+
password = "mypassword" # optional
127+
tls = false
128+
username = "default" # optional, defaults to "default"
129+
db = 0 # optional, defaults to 0
130+
```
131+
132+
Then start the MCP server with that profile:
133+
134+
```bash
135+
# Uses the default database profile from config
136+
redisctl mcp serve
137+
138+
# Or specify a profile explicitly
139+
redisctl -p local-redis mcp serve
100140
```
101141

142+
**Note**: If both `--database-url` and a database profile are available, the `--database-url` takes precedence.
143+
102144
## IDE Configuration
103145

104146
Choose your AI assistant below:

0 commit comments

Comments
 (0)