Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The Redis MCP Server is a **natural language interface** designed for agentic ap
- [Redis ACL](#redis-acl)
- [Configuration via command line arguments](#configuration-via-command-line-arguments)
- [Configuration via Environment Variables](#configuration-via-environment-variables)
- [EntraID Authentication for Azure Managed Redis](#entraid-authentication-for-azure-managed-redis)
- [Logging](#logging)
- [Integrations](#integrations)
- [OpenAI Agents SDK](#openai-agents-sdk)
Expand All @@ -56,6 +57,7 @@ The Redis MCP Server is a **natural language interface** designed for agentic ap
- **Full Redis Support**: Handles **hashes, lists, sets, sorted sets, streams**, and more.
- **Search & Filtering**: Supports efficient data retrieval and searching in Redis.
- **Scalable & Lightweight**: Designed for **high-performance** data operations.
- **EntraID Authentication**: Native support for Azure Active Directory authentication with Azure Managed Redis.
- The Redis MCP Server supports the `stdio` [transport](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio). Support to the `stremable-http` transport will be added in the future.

## Tools
Expand Down Expand Up @@ -349,6 +351,85 @@ If desired, you can use environment variables. Defaults are provided for all var
| `REDIS_SSL_CA_CERTS` | Path to the trusted CA certificates file | None |
| `REDIS_CLUSTER_MODE` | Enable Redis Cluster mode | `False` |

### EntraID Authentication for Azure Managed Redis

The Redis MCP Server supports **EntraID (Azure Active Directory) authentication** for Azure Managed Redis, enabling OAuth-based authentication with automatic token management.

#### Authentication Providers

**Service Principal Authentication** - Application-based authentication using client credentials:
```bash
export REDIS_ENTRAID_AUTH_FLOW=service_principal
export REDIS_ENTRAID_CLIENT_ID=your-client-id
export REDIS_ENTRAID_CLIENT_SECRET=your-client-secret
export REDIS_ENTRAID_TENANT_ID=your-tenant-id
```

**Managed Identity Authentication** - For Azure-hosted applications:
```bash
# System-assigned managed identity
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
export REDIS_ENTRAID_IDENTITY_TYPE=system_assigned

# User-assigned managed identity
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
export REDIS_ENTRAID_IDENTITY_TYPE=user_assigned
export REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID=your-identity-client-id
```

**Default Azure Credential** - Automatic credential discovery (recommended for development):
```bash
export REDIS_ENTRAID_AUTH_FLOW=default_credential
export REDIS_ENTRAID_SCOPES=https://redis.azure.com/.default
```

#### EntraID Configuration Variables

| Name | Description | Default Value |
|-----------------------------------------|-----------------------------------------------------------|--------------------------------------|
| `REDIS_ENTRAID_AUTH_FLOW` | Authentication flow type | None (EntraID disabled) |
| `REDIS_ENTRAID_CLIENT_ID` | Service Principal client ID | None |
| `REDIS_ENTRAID_CLIENT_SECRET` | Service Principal client secret | None |
| `REDIS_ENTRAID_TENANT_ID` | Azure tenant ID | None |
| `REDIS_ENTRAID_IDENTITY_TYPE` | Managed identity type | `"system_assigned"` |
| `REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID` | User-assigned managed identity client ID | None |
| `REDIS_ENTRAID_SCOPES` | OAuth scopes for Default Azure Credential | `"https://redis.azure.com/.default"` |
| `REDIS_ENTRAID_RESOURCE` | Azure Redis resource identifier | `"https://redis.azure.com/"` |

#### Key Features

- **Automatic token renewal** - Background token refresh with no manual intervention
- **Graceful fallback** - Falls back to standard Redis authentication when EntraID not configured
- **Multiple auth flows** - Supports Service Principal, Managed Identity, and Default Azure Credential
- **Enterprise ready** - Designed for Azure Managed Redis with centralized identity management

#### Example Configuration

For **local development** with Azure CLI:
```bash
# Login with Azure CLI
az login

# Configure MCP server
export REDIS_ENTRAID_AUTH_FLOW=default_credential
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
```

For **production** with Service Principal:
```bash
export REDIS_ENTRAID_AUTH_FLOW=service_principal
export REDIS_ENTRAID_CLIENT_ID=your-app-client-id
export REDIS_ENTRAID_CLIENT_SECRET=your-app-secret
export REDIS_ENTRAID_TENANT_ID=your-tenant-id
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
```

For **Azure-hosted applications** with Managed Identity:
```bash
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
export REDIS_ENTRAID_IDENTITY_TYPE=system_assigned
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
```

There are several ways to set environment variables:

Expand Down Expand Up @@ -471,6 +552,7 @@ You can also configure the Redis MCP Server in Augment manually by importing the

The simplest way to configure MCP clients is using `uvx`. Add the following JSON to your `claude_desktop_config.json`, remember to provide the full path to `uvx`.

**Basic Redis connection:**
```json
{
"mcpServers": {
Expand All @@ -487,6 +569,26 @@ The simplest way to configure MCP clients is using `uvx`. Add the following JSON
}
```

**Azure Managed Redis with EntraID authentication:**
```json
{
"mcpServers": {
"redis-mcp-server": {
"type": "stdio",
"command": "/Users/mortensi/.local/bin/uvx",
"args": [
"--from", "redis-mcp-server@latest",
"redis-mcp-server",
"--url", "redis://your-azure-redis.redis.cache.windows.net:6379"
],
"env": {
"REDIS_ENTRAID_AUTH_FLOW": "default_credential",
"REDIS_ENTRAID_SCOPES": "https://redis.azure.com/.default"
}
}
}
}
```

### VS Code with GitHub Copilot

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
"dotenv>=0.9.9",
"numpy>=2.2.4",
"click>=8.0.0",
"redis-entraid>=1.0.0",
]

[project.scripts]
Expand Down
130 changes: 130 additions & 0 deletions src/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

load_dotenv()

# Default values for Entra ID authentication
DEFAULT_TOKEN_EXPIRATION_REFRESH_RATIO = 0.9
DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 30000 # 30 seconds
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS = 10000 # 10 seconds
DEFAULT_RETRY_MAX_ATTEMPTS = 3
DEFAULT_RETRY_DELAY_MS = 100

REDIS_CFG = {
"host": os.getenv("REDIS_HOST", "127.0.0.1"),
"port": int(os.getenv("REDIS_PORT", 6379)),
Expand All @@ -20,6 +27,55 @@
"db": int(os.getenv("REDIS_DB", 0)),
}

# Entra ID Authentication Configuration
ENTRAID_CFG = {
# Authentication flow selection
"auth_flow": os.getenv(
"REDIS_ENTRAID_AUTH_FLOW", None
), # service_principal, managed_identity, default_credential
# Service Principal Authentication
"client_id": os.getenv("REDIS_ENTRAID_CLIENT_ID", None),
"client_secret": os.getenv("REDIS_ENTRAID_CLIENT_SECRET", None),
"tenant_id": os.getenv("REDIS_ENTRAID_TENANT_ID", None),
# Managed Identity Authentication
"identity_type": os.getenv(
"REDIS_ENTRAID_IDENTITY_TYPE", "system_assigned"
), # system_assigned, user_assigned
"user_assigned_identity_client_id": os.getenv(
"REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID", None
),
# Default Azure Credential Authentication
"scopes": os.getenv("REDIS_ENTRAID_SCOPES", "https://redis.azure.com/.default"),
# Token lifecycle configuration
"token_expiration_refresh_ratio": float(
os.getenv(
"REDIS_ENTRAID_TOKEN_EXPIRATION_REFRESH_RATIO",
DEFAULT_TOKEN_EXPIRATION_REFRESH_RATIO,
)
),
"lower_refresh_bound_millis": int(
os.getenv(
"REDIS_ENTRAID_LOWER_REFRESH_BOUND_MILLIS",
DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
)
),
"token_request_execution_timeout_ms": int(
os.getenv(
"REDIS_ENTRAID_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS",
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS,
)
),
# Retry configuration
"retry_max_attempts": int(
os.getenv("REDIS_ENTRAID_RETRY_MAX_ATTEMPTS", DEFAULT_RETRY_MAX_ATTEMPTS)
),
"retry_delay_ms": int(
os.getenv("REDIS_ENTRAID_RETRY_DELAY_MS", DEFAULT_RETRY_DELAY_MS)
),
# Resource configuration
"resource": os.getenv("REDIS_ENTRAID_RESOURCE", "https://redis.azure.com/"),
}


def parse_redis_uri(uri: str) -> dict:
"""Parse a Redis URI and return connection parameters."""
Expand Down Expand Up @@ -99,3 +155,77 @@ def set_redis_config_from_cli(config: dict):
else:
# Convert other values to strings
REDIS_CFG[key] = str(value) if value is not None else None


def set_entraid_config_from_cli(config: dict):
"""Update Entra ID configuration from CLI parameters."""
for key, value in config.items():
if value is not None:
if key in ["token_expiration_refresh_ratio"]:
# Keep float values as floats
ENTRAID_CFG[key] = float(value)
elif key in [
"lower_refresh_bound_millis",
"token_request_execution_timeout_ms",
"retry_max_attempts",
"retry_delay_ms",
]:
# Keep integer values as integers
ENTRAID_CFG[key] = int(value)
else:
# Convert other values to strings
ENTRAID_CFG[key] = str(value)


def is_entraid_auth_enabled() -> bool:
"""Check if Entra ID authentication is enabled."""
return ENTRAID_CFG["auth_flow"] is not None


def get_entraid_auth_flow() -> str:
"""Get the configured Entra ID authentication flow."""
return ENTRAID_CFG["auth_flow"]


def validate_entraid_config() -> tuple[bool, str]:
"""Validate Entra ID configuration based on the selected auth flow.

Returns:
tuple: (is_valid, error_message)
"""
auth_flow = ENTRAID_CFG["auth_flow"]

if not auth_flow:
return True, "" # No Entra ID auth configured, which is valid

if auth_flow == "service_principal":
required_fields = ["client_id", "client_secret", "tenant_id"]
missing_fields = [field for field in required_fields if not ENTRAID_CFG[field]]
if missing_fields:
return (
False,
f"Service principal authentication requires: {', '.join(missing_fields)}",
)

elif auth_flow == "managed_identity":
identity_type = ENTRAID_CFG["identity_type"]
if (
identity_type == "user_assigned"
and not ENTRAID_CFG["user_assigned_identity_client_id"]
):
return (
False,
"User-assigned managed identity requires user_assigned_identity_client_id",
)

elif auth_flow == "default_credential":
# Default credential doesn't require specific configuration
pass

else:
return (
False,
f"Invalid auth_flow: {auth_flow}. Must be one of: service_principal, managed_identity, default_credential",
)

return True, ""
29 changes: 28 additions & 1 deletion src/common/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from redis import Redis
from redis.cluster import RedisCluster

from src.common.config import REDIS_CFG
from src.common.config import REDIS_CFG, is_entraid_auth_enabled
from src.common.entraid_auth import (
create_credential_provider,
EntraIDAuthenticationError,
)
from src.version import __version__

_logger = logging.getLogger(__name__)
Expand All @@ -18,6 +22,17 @@ class RedisConnectionManager:
def get_connection(cls, decode_responses=True) -> Redis:
if cls._instance is None:
try:
# Create Entra ID credential provider if configured
credential_provider = None
if is_entraid_auth_enabled():
try:
credential_provider = create_credential_provider()
except EntraIDAuthenticationError as e:
_logger.error(
"Failed to create Entra ID credential provider: %s", e
)
raise

if REDIS_CFG["cluster_mode"]:
redis_class: Type[Union[Redis, RedisCluster]] = (
redis.cluster.RedisCluster
Expand All @@ -37,6 +52,12 @@ def get_connection(cls, decode_responses=True) -> Redis:
"lib_name": f"redis-py(mcp-server_v{__version__})",
"max_connections_per_node": 10,
}

# Add credential provider if available
if credential_provider:
connection_params["credential_provider"] = credential_provider
# Note: Azure Redis Enterprise with EntraID uses plain text connections
# SSL setting is controlled by REDIS_SSL environment variable
else:
redis_class: Type[Union[Redis, RedisCluster]] = redis.Redis
connection_params = {
Expand All @@ -56,6 +77,12 @@ def get_connection(cls, decode_responses=True) -> Redis:
"max_connections": 10,
}

# Add credential provider if available
if credential_provider:
connection_params["credential_provider"] = credential_provider
# Note: Azure Redis Enterprise with EntraID uses plain text connections
# SSL setting is controlled by REDIS_SSL environment variable

cls._instance = redis_class(**connection_params)

except redis.exceptions.ConnectionError:
Expand Down
Loading