Skip to main content
Glama
srdrcn
by srdrcn

redis-mcp

An MCP server for Redis fleets: one server in front of many Redis deployments, whether standalone servers, Redis Cluster or Sentinel-managed primaries. It follows the operating model of openshift-mcp-server and of its sibling kafka-mcp: one YAML config file, tools grouped into toolsets, a read-only switch (turned on in the example config), and Streamable HTTP or stdio as transport. It talks to Redis directly with redis-py.

Built with Python 3.10+ (the image uses Python 3.14), FastMCP from the MCP Python SDK 1.30 and redis-py 6.4.

Highlights

  • Many clusters, one server: standalone servers, Redis Cluster and Sentinel-managed primaries side by side. Every tool that works on Redis takes a cluster parameter whose enum lists exactly the clusters the tool may run on. When only one cluster is configured, the parameter is optional.

  • 55 tools in 6 toolsets: core, data, json, search, pubsub, docs.

  • Read-only in the example config: readOnly: true hides every write tool (one sandbox cluster opts back in), and disableDestructive: true hides the delete/pop/remove tools.

  • Passwords stay out of the config: each cluster's password comes from a mounted Secret file (<passwordsDir>/<name>_pass) or an environment variable (<name>_pass). hosts and options cannot carry one either.

  • Bounded calls: every tool call has a 15 s limit. The Redis clients use 5 s connect and 10 s socket timeouts and send every command once: there are no client retries, so the server never sends a write twice. A standalone server, a Sentinel-managed primary or a Redis Cluster node that stops answering therefore reports its own error (Timeout reading from socket) after 10 s. A Sentinel that does not answer costs 2 s. Redis Cluster startup nodes that do not answer while the client reads the slot map cost one timeout each and can still add up to the 15 s limit (see Time limits, retries and concurrency).

  • Clusters are isolated: each cluster has its own 8 workers for tool calls and its own connection pools (16 connections for the tools, 50 for pub/sub subscriptions), so a cluster that hangs does not slow down calls to the others, and open subscriptions do not take the connections the other tools need.

  • Simple to operate: a single static API key (in the config, or from a Secret through REDIS_MCP_API_KEY), JSON logs with one line per tool call, a /healthz endpoint, a non-root image on Docker Hub that runs on a read-only root filesystem, and kustomize manifests for OpenShift (restricted-v2 SCC).

  • Single-cluster mode: without a config file, the REDIS_* environment variables or the connection flags describe one Redis, with every toolset and stdio by default (see Single-cluster mode).

Contents

Related MCP server: Redis MCP

Quick start

Run it either with Python and uv or as a container with Docker/Podman (no Python needed on the host).

With Python

git clone https://github.com/srdrcn/redis-mcp.git && cd redis-mcp
uv sync                                          # creates .venv with the redis-mcp command
cp redis-mcp.example.yaml redis-mcp.local.yaml   # git-ignored; set apiKey and the clusters
export cbdevrds_pass='<password>'                # one <cluster>_pass variable per cluster (see Passwords)
uv run redis-mcp --config redis-mcp.local.yaml

The server refuses to start while apiKey still holds the <api-key> placeholder (exit code 2):

redis-mcp: Invalid config:
 - apiKey: apiKey still holds a placeholder (<...> or ${...}); set a real key, e.g. from `openssl rand -hex 32`

Set a real key, for example from openssl rand -hex 32, or an empty value (apiKey: "") to turn authentication off for a local run. To keep the key out of the file, delete the apiKey line and export REDIS_MCP_API_KEY instead (see Command line). The example config sets port: 8080, so the server listens on http://localhost:8080/mcp:

curl -s localhost:8080/healthz                   # {"status":"UP"}
curl -s localhost:8080/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "X-Authorization: $REDIS_MCP_KEY" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'   # REDIS_MCP_KEY = the key

To run over stdio instead (for a local MCP client that starts the process itself):

uv run redis-mcp --config redis-mcp.local.yaml --transport stdio

With Docker

Use the published image chosenwar/redis-mcp (see Container image) or build your own with docker build -t redis-mcp:latest .. Keep the container paths of the example config and mount the files there:

File

Path in the container

config

/etc/redis-mcp/config/redis-mcp.yaml

passwords

/etc/redis-mcp/passwords/<cluster>_pass, one file per cluster (the example config's passwordsDir); keep the directory outside the repository

CA certificates (optional)

/etc/redis-mcp/tls/, for tls: {caFile: /etc/redis-mcp/tls/ca.crt}

Streamable HTTP on http://localhost:8080/mcp:

docker run --rm -p 8080:8080 --read-only --tmpfs /tmp \
  -v "$PWD/redis-mcp.local.yaml:/etc/redis-mcp/config/redis-mcp.yaml:ro" \
  -v "$HOME/redis-mcp-passwords:/etc/redis-mcp/passwords:ro" \
  chosenwar/redis-mcp:latest /app/.venv/bin/redis-mcp --config /etc/redis-mcp/config/redis-mcp.yaml

curl -s localhost:8080/healthz                   # {"status":"UP"}

stdio (-i keeps stdin open):

docker run --rm -i --read-only --tmpfs /tmp \
  -v "$PWD/redis-mcp.local.yaml:/etc/redis-mcp/config/redis-mcp.yaml:ro" \
  -v "$HOME/redis-mcp-passwords:/etc/redis-mcp/passwords:ro" \
  chosenwar/redis-mcp:latest /app/.venv/bin/redis-mcp --config /etc/redis-mcp/config/redis-mcp.yaml --transport stdio

Notes:

  • The image's default command (/app/.venv/bin/python src/main.py) also works: pass -e REDIS_MCP_CONFIG=/etc/redis-mcp/config/redis-mcp.yaml instead of the command. Without that variable it runs the single-cluster mode from the REDIS_* variables.

  • To keep the API key out of the mounted file, delete apiKey there and add -e REDIS_MCP_API_KEY (which passes the variable from your shell).

  • The process runs as UID 1001 in group 0 and must be able to read the mounted files. Files with mode 600 owned by your user are not readable by UID 1001; run the container as your UID with --user "$(id -u):0".

  • Inside the container localhost is the container itself. To reach a Redis on the host, use --network host (Linux; then -p is not needed) or the host's address.

  • With Podman, use the same commands with podman; on SELinux hosts add :Z to the volume options (e.g. :ro,Z).

On OpenShift

The short version (each step is explained in OpenShift deployment):

oc new-project redis-mcp                                             # or: oc project <namespace>
# set the clusters in deploy/openshift/configmap.yaml (oc apply -k below creates the ConfigMap from it)
cp deploy/openshift/secret.yaml deploy/openshift/secret.local.yaml   # git-ignored (*.local.yaml)
openssl rand -hex 32                                                 # a value for api-key
# in secret.local.yaml: one <cluster>_pass per cluster and api-key, in place of the <...> placeholders
oc apply -f deploy/openshift/secret.local.yaml                       # Secrets redis-mcp-passwords, redis-mcp-api-key
oc apply -k deploy/openshift                                         # image: docker.io/chosenwar/redis-mcp:latest
oc rollout status deployment/redis-mcp
oc get route redis-mcp                                               # MCP endpoint: https://<route-host>/mcp

Clients send the api-key value in the X-Authorization header; see Claude Code for the .mcp.json entry. After a change to either file, apply it again and run oc rollout restart deployment/redis-mcp.

Command line

Option

Environment variable

Meaning

--config <file>

REDIS_MCP_CONFIG

Main config file (YAML).

--config-dir <dir>

REDIS_MCP_CONFIG_DIR

Directory of drop-in .yaml/.yml files merged over the main config (see Drop-in files). Can be used without --config.

--transport <stdio|http>

Transport. Defaults to http when an HTTP port is set (> 0) and to stdio otherwise. --transport http without a port uses 8080.

--http-port <port>

HTTP port (1–65535). Overrides port from the config. (--port is the Redis port of the single-cluster mode.)

--api-key <key>

REDIS_MCP_API_KEY

API key that HTTP clients must send in X-Authorization. Overrides apiKey from the config, also in the single-cluster mode. An empty value is ignored, so an empty variable cannot turn authentication off. A value of only whitespace stops the server (redis-mcp: --api-key / REDIS_MCP_API_KEY is blank; set a key, or leave it empty or unset to keep apiKey from the config); any other value is checked like apiKey (redis-mcp: --api-key / REDIS_MCP_API_KEY: apiKey has leading or trailing whitespace ...). Prefer the variable: a command line can be read by other users (ps) and anyone who can read the Deployment.

--help

Print usage.

The connection flags (--url, --host, --port, ...) and the --entraid-* flags belong to the single-cluster mode. Both --option value and --option=value work, and an option wins over its environment variable.

With --config or --config-dir, the clusters come from the config only: the connection flags (--url, --host, --port, --db, --username, --password, --ssl*, --cluster-mode) are rejected, for example redis-mcp: --host cannot be combined with --config/--config-dir; define the clusters in the config file, and the REDIS_HOST/REDIS_PORT/... variables are not used. An empty --url and the literal --url '${REDIS_URL}' (which some MCP clients pass when REDIS_URL is not set) count as not given. The --entraid-* flags and REDIS_ENTRAID_* variables still apply to clusters with entraid: true.

The process writes redis-mcp: ... to stderr and exits with code 2 for: an invalid or unreadable config (also one that is not UTF-8: Could not read config file: <path> (not UTF-8: invalid byte on line <n>); a UTF-8 byte order mark is accepted) or password file, a blank or invalid --api-key, a connection flag combined with --config, an unknown toolset, and a cluster with entraid: true but no Entra ID auth flow. A malformed option value gets click's usage error, also with exit code 2. python src/main.py (the image's default command) takes no options and reads REDIS_MCP_CONFIG/REDIS_MCP_CONFIG_DIR and REDIS_MCP_API_KEY from the environment.

Other environment variables:

Variable

Meaning

REDIS_MCP_LOG_LEVEL

Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL (also WARN, FATAL or a number). The default is INFO over HTTP and WARNING over stdio (see JSON logs).

<name>_pass, <name>_sentinel_pass

Cluster passwords, when there is no such file in passwordsDir (see Passwords).

MCP_DOCS_SEARCH_URL

Endpoint of the docs toolset (default https://redis.io/convai/api/docs/search).

REDIS_ENTRAID_*

Entra ID settings for clusters with entraid: true (see Entra ID).

A .env file in the repository root is loaded too (python-dotenv); variables that are already set win.

Configuration

This is the content of redis-mcp.example.yaml without its header comment, the example for a local or Docker run. On OpenShift, deploy/openshift/configmap.yaml holds the same settings and clusters without the apiKey line: the key comes from the Secret redis-mcp-api-key there (see OpenShift deployment). Never commit real keys: keep a filled-in copy in redis-mcp.local.yaml or redis-mcp.yaml (both git-ignored).

port: 8080
# X-Authorization value: a long random key (openssl rand -hex 32), or "" for no auth. Better, keep the key out of
# this file: delete the line and set REDIS_MCP_API_KEY (on OpenShift from a Secret, see deployment.yaml).
apiKey: <api-key>
readOnly: true
disableDestructive: true
toolsets: [core, data]
passwordsDir: /etc/redis-mcp/passwords   # <name>_pass files (the redis-mcp-passwords Secret); else env <name>_pass

clusters:
  - name: cbdevrds
    mode: standalone
    hosts: cbdevrds.example.com:6379   # host:port only: no redis:// scheme, no user:password@
    db: 0
    username: mcp-reader
  - name: cbtstrds
    mode: standalone
    hosts: cbtstrds.example.com:6379
    username: mcp-reader
  - name: cbprdrds
    mode: cluster                  # Redis Cluster: startup nodes, the client discovers the rest
    hosts: [cbprdrds-1.example.com:6379, cbprdrds-2.example.com:6379, cbprdrds-3.example.com:6379]
    username: mcp-reader
  - name: cbsbxrds
    mode: standalone
    hosts: cbsbxrds.example.com:6379
    username: mcp-writer           # an ACL user that may write; the sandbox is the only writable cluster
    readOnly: false
    # modules: [json, search]      # lists the json/search tools for this cluster (Redis 8, Redis Stack)
    # tls:                         # or `tls: true` for TLS with the system CAs
    #   caFile: /etc/redis-mcp/tls/ca.crt   # caPath is not supported with mode: cluster
    # options:                     # client tuning; keys and values depend on the mode (README, "Cluster options")
    #   socket_timeout: 5          # seconds, > 0 and <= 3600
  # A primary managed by Redis Sentinel. A Sentinel that does not answer costs 2 s. The Sentinels' own
  # password is <name>_sentinel_pass, so no other cluster may be named cbuatrds_sentinel:
  # - name: cbuatrds
  #   mode: sentinel
  #   hosts: [cbuatrds-s1.example.com:26379, cbuatrds-s2.example.com:26379, cbuatrds-s3.example.com:26379]
  #   masterName: mymaster
  #   username: mcp-reader

Top-level keys

Key

Type

Default

Meaning

port

integer (0–65535)

not set

HTTP port. When it is set (> 0) and --transport is not given, the server runs Streamable HTTP, otherwise stdio. --http-port overrides it.

apiKey

string

not set

Static API key. HTTP requests to /mcp must send it in the X-Authorization header. Not set or empty ("") turns the check off. A non-empty key must not be blank, must not start or end with whitespace and must not be a <...> or ${...} placeholder, otherwise the server does not start; a key shorter than 16 characters only logs a warning. A non-empty --api-key / REDIS_MCP_API_KEY overrides it (the config is still checked first, so a placeholder left here stops the server even then).

readOnly

boolean

false

true hides every write tool on clusters that do not override it with their own readOnly.

disableDestructive

boolean

false

true hides the destructive tools (delete, hdel, lpop, rpop, lrem, srem, zrem, xdel, xgroup_destroy, json_del) on every cluster. It is global only and does not affect the other write tools.

toolsets

list of strings

[core]

Toolsets to enable, which also sets the order of tools/list. An unknown name stops the server at startup; an empty or duplicate entry is an invalid config.

passwordsDir

path

/etc/redis-mcp/passwords

Directory of the <name>_pass password files (see Passwords).

clusters

list

(required)

The Redis deployments. At least one is required, and names must be unique.

The defaults in the code are permissive (readOnly: false, disableDestructive: false). The example config turns both on.

YAML numbers keep the text they are written with wherever a string is expected, so apiKey: 0123456701234567 or username: 0123 are read exactly as written (plain YAML 1.1 would turn them into octal numbers); port and db are converted to numbers. In options, values that look like integers or decimals are passed to redis-py as numbers, quoted or not, except client_name, which stays text (client_name: 0123 is the name 0123); every option value is then checked (see Cluster options). Unknown keys are errors, and the error message names the field (clusters[0].password: Extra inputs are not permitted). These messages do not repeat the rejected value, so a password typed into the wrong field does not reach the log.

Cluster keys

Key

Type

Default

Meaning

name

string

(required)

Unique name of 1–63 characters from A-Z a-z 0-9 _ . -, starting with a letter or digit. It becomes a value of the cluster enum, and it names the password file or variable (<name>_pass). A cluster must not be named <x>_sentinel while a mode: sentinel cluster <x> exists, because <x>_sentinel_pass would be both its password and the Sentinel password of <x> (cluster name <x>_sentinel clashes with the Sentinel password of cluster <x> (<x>_sentinel_pass); rename one of them).

mode

standalone, cluster or sentinel

standalone

standalone: redis.Redis. cluster: redis.cluster.RedisCluster (Redis Cluster / OSS cluster API). sentinel: redis.sentinel.Sentinel(...).master_for(masterName), always the current primary.

hosts

string or list

(required)

host:port entries, as a comma-separated string or a YAML list. IPv6 addresses are written [addr]:port (quote them in YAML). standalone takes exactly one; cluster takes the startup nodes (the client reads the slot map from them and discovers the other nodes); sentinel takes the Sentinels. A value that contains a scheme (://) or credentials (@) anywhere is rejected with hosts takes host:port entries without a scheme or credentials (user:password@); passwords come from the passwords directory. Other errors name the entry by its position (1 for the first) and never repeat it, since it may hold a password: hosts entry 2 is not host:port (or [IPv6 address]:port) (also for an empty entry or a space inside the host) and hosts entry 2: the port must be between 1 and 65535. Prefer fully qualified host names (see DNS lookups).

db

integer (≥ 0)

0

Database number (standalone, sentinel). A Redis Cluster only has database 0, so db other than 0 is rejected with mode: cluster.

username

string

not set

ACL user (redis-py username). Without it, the password authenticates the default user (requirepass). Cannot be combined with entraid.

masterName

string

not set

Name of the monitored primary. Required with mode: sentinel, rejected with the other modes.

tls

true or map

not set (no TLS)

TLS for the connections to Redis (redis-py ssl=True plus the settings below). true uses the defaults: the server certificate is verified against the system CAs, including the host name. With mode: sentinel the same settings also apply to the Sentinel connections. With mode: cluster, caPath is not supported.

readOnly

boolean

global readOnly

Per-cluster override of readOnly, in either direction.

modules

list of json, search

[]

Modules the cluster has. The json and search toolsets offer their tools only on clusters that list the module.

entraid

boolean

false

Authenticate with the Entra ID credential provider (settings from the --entraid-* flags / REDIS_ENTRAID_* variables, see Entra ID) instead of a password. It needs an Entra ID auth flow (--entraid-auth-flow / REDIS_ENTRAID_AUTH_FLOW): without one the server does not start (redis-mcp: clusters <names> set entraid: true, but no Entra ID auth flow is configured (REDIS_ENTRAID_AUTH_FLOW or --entraid-auth-flow)) instead of connecting without authentication.

options

map

{}

Client tuning for redis-py, limited to the keys of Cluster options for the cluster's mode. Applied after the defaults, the credentials and the TLS settings.

Cluster options

options accepts only the redis-py arguments below. Addresses, db, username and tls have their own keys, passwords come from the passwords directory, and the server sets decode_responses and retry itself. Any other key, or a key of another mode, stops the server at startup:

redis-mcp: Invalid config:
 - clusters[0]: options password, retry_on_timeout not supported in standalone mode (supported: socket_timeout, socket_connect_timeout, socket_keepalive, max_connections, client_name, protocol, health_check_interval; addresses, db, username and tls have their own keys, passwords come from the passwords directory)

The values are checked as well. A value outside the rule in the "Value" column, null included (a null timeout would remove the time bound), stops the server with options.<key> must be <rule>, for example clusters[0]: options.socket_timeout must be a number of seconds > 0 and <= 3600.

Option

Modes

Default

Value

Meaning

socket_timeout

all

10

a number of seconds > 0 and <= 3600

Seconds to wait for a reply. With mode: sentinel the Sentinel connections use it too, but at most 2 s.

socket_connect_timeout

all

5

a number of seconds > 0 and <= 3600

Seconds to wait for a connection. With mode: sentinel the Sentinel connections use it too, but at most 2 s. The host name lookup is not part of it (see DNS lookups).

socket_keepalive

all

off

true or false

TCP keepalive on the connections to Redis.

max_connections

all

16

an integer >= 1

Size of the connection pool of the tools' client (with mode: cluster, per node). Keep it above the 8 calls a cluster runs at a time, or calls fail with Too many connections. Pub/sub subscriptions have a client of their own with 50 connections, which this option does not change.

client_name

all

not set

a name without spaces (not empty)

Connection name set with CLIENT SETNAME, shown by CLIENT LIST. Kept as text, also when it looks like a number.

protocol

all

2

2 or 3

RESP protocol version.

health_check_interval

standalone, sentinel

0 (off)

a number of seconds >= 0

A connection that was idle for longer than this many seconds is checked with PING before it is used.

read_from_replicas

cluster

false

true or false

Send read commands to the replicas as well (round robin); replicas can return slightly older data. redis-py 6 marks the argument as deprecated.

require_full_coverage

cluster

true

true or false

false also accepts a cluster whose hash slots are not all covered.

reinitialize_steps

cluster

5

an integer >= 0

Number of MOVED redirects after which the whole slot map is read again (1: after every one, 0: never).

dynamic_startup_nodes

cluster

true

true or false

After connecting, use the discovered nodes as startup nodes for later slot map refreshes. Set false when hosts is a DNS name or load balancer address that CLUSTER SLOTS does not list.

health_check_interval is not accepted with mode: cluster, and the last four keys only with mode: cluster, because redis-py would drop or reject them there.

TLS keys

Key

Type

Default

redis-py argument

Meaning

caFile

path

not set

ssl_ca_certs

CA bundle (PEM) trusted in addition to the system CAs.

caPath

path

not set

ssl_ca_path

Directory of CA certificates. Not supported with mode: cluster, because redis-py's RedisCluster ignores it and would verify the nodes against the system CAs only: the server does not start (tls.caPath is not supported in cluster mode; use caFile).

certFile

path

not set

ssl_certfile

Client certificate (mutual TLS).

keyFile

path

not set

ssl_keyfile

Key of the client certificate.

certReqs

required, optional or none

required

ssl_cert_reqs

Verification of the server certificate. none also turns the host name check off.

checkHostname

boolean

not set (redis-py default: on)

ssl_check_hostname

Match the server certificate against the host name in hosts.

Redis clients

The client arguments of each cluster are built in this order (later entries win):

# 1. defaults
decode_responses=True or False          # chosen by the tool
lib_name="redis-py(redis-mcp_v<version>)"
socket_connect_timeout=5
socket_timeout=10
# 2. credentials: username and the <name>_pass password, or credential_provider with entraid: true
# 3. tls: ssl=True, ssl_cert_reqs, ssl_ca_certs, ssl_ca_path, ssl_certfile, ssl_keyfile, ssl_check_hostname
# 4. the cluster's options: map (only the keys of "Cluster options")
max_connections=16                      # tools' client, only if options does not set it
max_connections=50                      # pub/sub client, always
# 5. set by the server (options cannot change it)
retry=Retry(NoBackoff(), 0)             # one attempt per command, in every mode (also for the Sentinels)
  • A cluster has up to three clients: one for the tools, one with decode_responses=False for get_vector_from_hash (which reads raw bytes), and one for pub/sub subscriptions. Each is created on first use and then reused. Each client has its own lock, so a cluster that is slow to connect does not hold up calls for the others.

  • A failed creation leaves no client behind, but its error is remembered for 5 s: calls in that time, including those that were waiting for the creation, get the same error at once, and the first call after it tries again. Only a Redis Cluster client connects while it is created (it reads the slot map), so this mostly spares a burst of calls to an unreachable Redis Cluster from repeating the slow connection attempt one after another.

  • The tools' client has a pool of up to 16 connections (max_connections): room for the 8 calls a cluster runs at a time and the clusters_list checks. Pub/sub subscriptions hold their connection until unsubscribe, so they use a client of their own with up to 50 connections (with mode: cluster, per node), the most subscriptions the server allows in total. Open subscriptions therefore never take the connections the other tools need.

  • Sentinel: the Sentinel connections use the cluster's socket timeouts (including overrides from options), but at most 2 s each, its tls settings, one attempt per command and the password <name>_sentinel_pass (no username). The other options apply only to the connections to the primary. When no Sentinel names the primary, the error lists each Sentinel that could not be asked as <host>:<port> - <error type>: <message>, for example No master found for 'mymaster' : 10.0.0.5:26379 - TimeoutError: Timeout connecting to server. (redis-py's own message would show the Sentinel clients' connection arguments, the Sentinel password included.)

  • What a single attempt per command means for restarts, failovers and slow or unreachable clusters is described in Time limits, retries and concurrency.

Drop-in files (--config-dir)

  • The server reads the regular *.yaml and *.yml files directly inside the directory. Hidden files are skipped and subdirectories are not read, so the directory can be a mounted ConfigMap. The files are sorted by name and merged over --config one after another. --config-dir can also be used on its own.

  • Maps are merged deeply. clusters is merged by name: an entry with an existing name overrides only the fields it sets (its tls and options maps are merged key by key), and an entry with a new name is appended. Every other list, such as toolsets, modules or a hosts list, replaces the earlier value entirely. Scalars replace the earlier value.

  • Validation runs after the merge.

# conf.d/10-sandbox.yaml: the sandbox runs Redis 8 with the JSON and search modules
clusters:
  - name: cbsbxrds
    modules: [json, search]
# conf.d/20-toolsets.yaml: replaces the whole toolsets list
toolsets: [core, data, json, search]

The config is read once at startup, so restart the server to apply changes.

Passwords

Passwords are never part of the YAML: there is no password key, options does not accept password, and hosts rejects user:password@. At startup, for each cluster <name>, the server reads:

  1. the file <passwordsDir>/<name>_pass (for example /etc/redis-mcp/passwords/cbdevrds_pass) if it exists,

  2. otherwise the environment variable <name>_pass.

Trailing line breaks are removed from a file's content; everything else, including spaces, is part of the password. A file that is not UTF-8 stops the server (Could not read password file <path> (not UTF-8)). With mode: sentinel, the password of the Sentinels themselves is read the same way from <name>_sentinel_pass (this is why no other cluster may be named <name>_sentinel). A cluster without a password (and without entraid: true) connects without AUTH, and the server logs a warning at startup:

cluster cbdevrds: no password (/etc/redis-mcp/passwords/cbdevrds_pass or env cbdevrds_pass); connecting without AUTH

The passwords are read only at startup. The auth field of clusters_list shows password, none or entraid instead of the password.

On OpenShift, the passwords are the keys of the Secret redis-mcp-passwords in deploy/openshift/secret.yaml (one <cluster>_pass key per cluster, plus <cluster>_sentinel_pass for Sentinels that require a password), mounted at /etc/redis-mcp/passwords, one file per key. secret.yaml holds only <password> placeholders and is not part of the kustomization; fill in a git-ignored copy and apply it:

cp deploy/openshift/secret.yaml deploy/openshift/secret.local.yaml   # git-ignored (*.local.yaml)
# replace each "<password>" in secret.local.yaml (and the api-key, see OpenShift deployment)
oc apply -f deploy/openshift/secret.local.yaml
oc rollout restart deployment/redis-mcp                              # when the pod already runs

The values are YAML strings under stringData: keep the quotes, so that a password such as 0123, yes or one with # stays text as written. Inside double quotes, write \\ for a backslash and \" for a double quote; inside single quotes, only ' changes (written ''). The value gets no trailing line break. For a local run, export cbdevrds_pass=... works in bash (names with - or . cannot be exported from a shell; use files then).

Redis ACL user per cluster

readOnly only decides which tools are listed; it does not restrict what the server's Redis user may do. Give the server its own ACL user on every cluster, with only the rights it needs. A read-only user for the core and data toolsets:

ACL SETUSER mcp-reader on >REPLACE_ME ~* &* +@read +@connection -@dangerous +info +client|list +cluster|slots +cluster|nodes +cluster|info

and, on a writable cluster such as the sandbox, a user that may also write but still cannot flush, reconfigure or shut down the server:

ACL SETUSER mcp-writer on >REPLACE_ME ~* &* +@read +@write +@connection -@dangerous +info +client|list +cluster|slots +cluster|nodes +cluster|info
  • The rules are applied from left to right. -@dangerous must come before +info +client|list: INFO and CLIENT LIST (used by the info, client_list and clusters_list tools) are in the @dangerous category, so a -@dangerous at the end would remove them again. -@dangerous also removes KEYS, FLUSHALL, FLUSHDB, CONFIG, SHUTDOWN and CLIENT KILL.

  • +cluster|slots is needed on a Redis Cluster: redis-py reads the slot map when it connects, and without this command the connection fails with Cluster mode is not enabled on this node. +cluster|nodes +cluster|info let cluster_nodes read the node list and the cluster state. They do no harm elsewhere.

  • Checked on Redis 7.0 with the server's tools and ACL DRYRUN: mcp-reader can run get, hgetall, scan_keys, scan_all_keys, type, dbsize, info, client_list and clusters_list, and set/delete fail with this user has no permissions to run the 'set' command; mcp-writer can also run set, delete, expire, rename and the stream group commands, but not FLUSHALL, FLUSHDB, KEYS, CONFIG or SHUTDOWN.

  • The pubsub toolset needs +subscribe +psubscribe +unsubscribe +punsubscribe (and +publish for the write tool publish): pub/sub commands are not in @read or @write.

  • The json and search tools use module commands (JSON.GET, JSON.SET, JSON.DEL, FT._LIST, FT.INFO, FT.SEARCH, FT.CREATE). Their ACL categories depend on the Redis/module version, so check them with ACL DRYRUN mcp-reader JSON.GET somekey and add the commands explicitly if needed.

  • ACL users are per node: on a Redis Cluster or a Sentinel setup, create the user on every node, replicas included (or use an ACL file), so that it still exists after a failover. Persist it with ACL SAVE (with an aclfile) or CONFIG REWRITE.

  • Set username: mcp-reader (or mcp-writer) on the cluster in the config, and put the password in <name>_pass.

Toolsets

Toolset

Tools

What it covers

Available on

core (enabled by default)

11

The cluster list with a connection check, the nodes of a cluster (cluster_nodes), server information (info, dbsize, client_list) and key operations (type, scan_keys, scan_all_keys, expire, rename, delete)

Every cluster. clusters_list takes an optional cluster (all clusters when omitted) and check (default true: connect and report status, latency, Redis version, role and, for a Redis Cluster, the number of primaries).

data

29

Strings, hashes (including vectors stored as binary fields), lists, sets, sorted sets, and streams with consumer groups

Every cluster

json

3

RedisJSON documents: get, set and delete at a path

Clusters with modules: [json]

search

6

Redis Query Engine: indexes, index information, indexed key count, vector index creation on hashes, vector (KNN) and hybrid search

Clusters with modules: [search]

pubsub

5

Publish, subscribe to a channel or a pattern, read the queued messages, unsubscribe

Every cluster. read_messages and unsubscribe take the subscription_id and no cluster.

docs

1

Search the redis.io documentation

Not tied to a cluster (no cluster parameter). Calls MCP_DOCS_SEARCH_URL (default https://redis.io/convai/api/docs/search), so the pod needs egress to it.

Pub/sub subscriptions live in the server process: at most 50 at a time over all clusters (one more gets Too many active subscriptions. Close unused subscriptions and try again.), and a subscription that was not read for 5 minutes is closed when the next one is created. They use the cluster's pub/sub client, not the connections of the other tools (see Redis clients). read_messages waits up to timeout_ms (default 1000, at most 5000; 0 does not wait) and returns once max_messages messages (default 10, at most 100) have arrived or the time is up; a call right after subscribe waits like any other. Subscriptions are lost when the pod restarts, and read_messages then answers Subscription '<id>' was not found; this is why the Deployment runs a single replica.

On a Redis Cluster, dbsize adds up the keys of every primary, and scan_all_keys scans every primary until each one is done. scan_keys runs one SCAN step on every primary at once (count applies to each of them) and returns what they found together, with cursor: 0. scan_complete is true only when every primary has finished; otherwise a note points to scan_all_keys. Every primary has its own cursor, so such a scan cannot be continued: a call with a cursor other than 0 returns Error: scan_keys cannot continue a cursor on a Redis Cluster (every node has its own cursor); call it with cursor 0, or use scan_all_keys. On a Redis Cluster, use scan_all_keys with a narrow pattern (it still has to finish within the 15 s limit).

Which tools are listed

For every tool, the server works out which clusters it may run on:

  1. Its toolset must be in toolsets.

  2. json and search tools need a cluster that lists the module in modules.

  3. Write tools (descriptions starting with WRITE:) need the cluster's effective readOnly to be false. The cluster's own readOnly applies if it is set, otherwise the global one does.

  4. Destructive tools also need disableDestructive: false.

A tool with no eligible cluster is not listed at all (calling it anyway returns Unknown tool: <name>). Every other tool is listed with its cluster enum narrowed to the eligible clusters, and a call that names any other cluster is rejected by schema validation (Input should be 'cbsbxrds'). With two or more clusters configured, cluster is required and comes first, so a call without it is rejected as well (Field required). With exactly one cluster (a one-cluster config file or the single-cluster mode), cluster is optional and comes last, described as Cluster name (default: the only configured cluster): a call without it runs on that cluster. clusters_list always takes an optional cluster (all clusters when omitted), and read_messages, unsubscribe and search_redis_documents take none. With the example config, 31 of the 55 tools are listed. For example:

Tool

cluster enum

get, scan_keys, info (read)

cbdevrds, cbtstrds, cbprdrds, cbsbxrds

set, hset, expire (write)

cbsbxrds

delete, hdel, lpop (destructive)

not listed

json_get, get_indexes

not listed (toolsets json/search not enabled)

The WRITE: prefix is added to the description of every write and destructive tool. Tools also carry MCP annotations: readOnlyHint: true on read tools and destructiveHint: true on destructive tools (both are false on the other write tools).

Tool reference

The tables list every tool in the order of tools/list within its toolset. Kind: read tools are always listed, write tools (the description starts with WRITE:, which is left out below) are hidden by readOnly, and destructive tools are hidden by readOnly or disableDestructive. Needs is what a tool needs beyond a cluster from its enum. The description is the first paragraph of the tool's own description.

Toolset

Tools

Read

Write

Destructive

core

11

8

2

1

data

29

10

11

8

json

3

1

1

1

search

6

5

1

0

pubsub

5

4

1

0

docs

1

1

0

0

Total

55

29

16

10

core (11 tools)

Tool

Kind

Needs

Description

clusters_list

read

cluster optional (all clusters when omitted)

List the configured Redis clusters (mode, hosts, readOnly, modules, auth) and, unless check is false, whether each one answers: status, latency, Redis version and role. Start here when a cluster misbehaves

cluster_nodes

read

—

Show the nodes of a cluster and their state: on a Redis Cluster the CLUSTER INFO summary and every node (address, id, primary or replica, the primary a replica follows, slot ranges, pfail/fail flags, link); on standalone and Sentinel the replication view (role, primary, connected replicas with lag)

delete

destructive

—

Delete a Redis key

type

read

—

Returns the string representation of the type of the value stored at key

expire

write

—

Set an expiration time for a Redis key

rename

write

—

Renames a Redis key from old_key to new_key

scan_keys

read

—

Scan keys in the Redis database using the SCAN command (non-blocking, production-safe)

scan_all_keys

read

—

Scan and return ALL keys matching a pattern using multiple SCAN iterations

dbsize

read

—

Get the number of keys stored in the Redis database

info

read

—

Get Redis server information and statistics

client_list

read

—

Get a list of connected clients to the Redis server

data (29 tools)

Tool

Kind

Needs

Description

hset

write

—

Set a field in a hash stored at key with an optional expiration time

hget

read

—

Get the value of a field in a Redis hash

hdel

destructive

—

Delete a field from a Redis hash

hgetall

read

—

Get all fields and values from a Redis hash

hexists

read

—

Check if a field exists in a Redis hash

set_vector_in_hash

write

—

Store a vector as a field in a Redis hash

get_vector_from_hash

read

—

Retrieve a vector from a Redis hash and convert it back from binary blob

lpush

write

—

Push a value onto the left of a Redis list and optionally set an expiration time

rpush

write

—

Push a value onto the right of a Redis list and optionally set an expiration time

lpop

destructive

—

Remove and return the first element from a Redis list

rpop

destructive

—

Remove and return the last element from a Redis list

lrange

read

—

Get elements from a Redis list within a specific range

llen

read

—

Get the length of a Redis list

lrem

destructive

—

Remove elements from a Redis list

sadd

write

—

Add a value to a Redis set with an optional expiration time

srem

destructive

—

Remove a value from a Redis set

smembers

read

—

Get all members of a Redis set

zadd

write

—

Add a member to a Redis sorted set with an optional expiration time

zrange

read

—

Retrieve a range of members from a Redis sorted set

zrem

destructive

—

Remove a member from a Redis sorted set

xadd

write

—

Add an entry to a Redis stream with an optional expiration time

xrange

read

—

Read entries from a Redis stream

xdel

destructive

—

Delete an entry from a Redis stream

xgroup_create

write

—

Create a consumer group for a Redis stream

xgroup_destroy

destructive

—

Destroy a consumer group for a Redis stream

xreadgroup

write

—

Read entries from a Redis stream using a consumer group

xack

write

—

Acknowledge entries that were processed by a consumer group

set

write

—

Set a Redis string value with an optional expiration time

get

read

—

Get a Redis string value

json (3 tools)

Tool

Kind

Needs

Description

json_set

write

modules: [json]

Set a JSON value in Redis at a given path with an optional expiration time

json_get

read

modules: [json]

Retrieve a JSON value from Redis at a given path

json_del

destructive

modules: [json]

Delete a JSON value from Redis at a given path

search (6 tools)

Tool

Kind

Needs

Description

get_indexes

read

modules: [search]

List of indexes in the Redis database

get_index_info

read

modules: [search]

Retrieve schema and information about a specific Redis index using FT.INFO

get_indexed_keys_number

read

modules: [search]

Retrieve the number of indexed keys by the index

create_vector_index_hash

write

modules: [search]

Create a Redis 8 vector similarity index using HNSW on a Redis hash

vector_search_hash

read

modules: [search]

Perform a KNN vector similarity search using Redis 8 or later version on vectors stored in hash data structures

hybrid_search

read

modules: [search]

Perform a hybrid search combining a Redis filter expression with KNN vector similarity

pubsub (5 tools)

Tool

Kind

Needs

Description

publish

write

—

Publish a message to a Redis channel

subscribe

read

—

Subscribe to a Redis channel and return a reusable subscription handle

psubscribe

read

—

Subscribe to Redis channels using a pattern

read_messages

read

no cluster; takes the subscription_id

Read pending pub/sub messages for an existing subscription

unsubscribe

read

no cluster; takes the subscription_id

Unsubscribe and close an existing pub/sub subscription

docs (1 tool)

Tool

Kind

Needs

Description

search_redis_documents

read

no cluster; egress to MCP_DOCS_SEARCH_URL

Search Redis documentation and knowledge base to learn about Redis concepts and use cases

Time limits, retries and concurrency

Every tool call runs for at most 15 s. A call that runs longer returns:

Tool did not finish within 15 s (timeout). The cluster may be slow or unreachable (check clusters_list), or the request too large (narrow the pattern or count).

The call's worker keeps running until the Redis client gives up, so a write that hit the limit may still be applied. clusters_list checks all clusters in parallel and stops waiting after 13 s; a cluster whose check has not finished by then shows status: timeout (The connection check did not finish within the time limit).

Client timeouts, no retries. The Redis clients wait 5 s for a connection (socket_connect_timeout) and 10 s for a reply (socket_timeout); both can be changed per cluster in options. Every command is sent once, in every mode and also to the Sentinels: redis-py's default retry policy (3 retries with backoff, timeouts included) is replaced by a single attempt. A command whose connection broke or timed out may already have run, so sending it again could apply a write twice, and retries could outlast the 15 s limit.

  • A call whose connection breaks while its command is in flight fails once with the client's error, for example Error while reading from <host>:<port> : (104, 'Connection reset by peer') when the server was killed, or Timeout reading from socket when the server or its host went away without closing the connection. So the first call after a restart or a failover can fail once; the next call opens a new connection (on Sentinel, to the primary the Sentinels name then).

  • Connections that broke while they sat idle in the pool are still replaced: the pool checks a connection before it hands it out and reconnects when the server has closed it. Measured: after a restart of a standalone server, and after SENTINEL FAILOVER (the Sentinels close the clients of the old primary), the next call succeeded.

  • Redis Cluster: after a connection error or a timeout, the client removes that node from its startup nodes and reads the slot map again from the others before it reports the error, so the next call uses the new topology. MOVED and ASK redirects are still followed.

  • Sentinel: each new connection to the primary first asks the Sentinels, in turn, which node is the primary. Each Sentinel connection uses connect and reply timeouts of at most 2 s (socket_connect_timeout and socket_timeout of the cluster, capped at 2 s), so a Sentinel that does not answer costs 2 s. A Sentinel that answers slowly can take longer: the connection waits up to 2 s for each reply (AUTH, client info, the master lookup).

What a cluster that fails returns. Measured with the default timeouts, redis-py 6.4 and Redis 7.0, with a closed port, with a listener whose backlog is full (connections time out, as with a firewall that drops packets), with a listener that accepts connections but never answers, and with DEBUG SLEEP on a running server:

Failure

standalone

sentinel

cluster

Connection refused

Error 111 connecting to <host>:<port>. Connection refused. at once

No master found for '<masterName>' : <host>:<port> - ConnectionError: Error 111 connecting to <host>:<port>. Connection refused. (one entry per Sentinel) at once

Redis Cluster cannot be connected. Please provide at least one reachable node: Error 111 ... at once

Connection times out

Timeout connecting to server after 5 s

2 s per Sentinel: No master found for '<masterName>' : <host>:<port> - TimeoutError: Timeout connecting to server, ... after 2 s with one Sentinel, 6 s with three

5 s per startup node: with one or two nodes Redis Cluster cannot be connected ...: Timeout connecting to server after 5 or 10 s, with three or more the 15 s limit

Connection accepted, no answer

Timeout reading from socket after 10 s

2 s per Sentinel: No master found for '<masterName>' : <host>:<port> - TimeoutError: Timeout reading from socket, ... after 2 s with one Sentinel, 6 s with three

10 s per startup node: with one node Redis Cluster cannot be connected ...: Timeout reading from socket after 10 s, with two or more the 15 s limit

Server hangs after the client connected

Timeout reading from socket after 10 s

the primary: Timeout reading from socket after 10 s

a node: Timeout reading from socket after 10 s for keys on that node; keys on the other nodes are not affected

The Sentinel and startup node rows apply when none of them answers. One that answers ends the search: with a hanging Sentinel listed before a working one, the first call took 2 s and succeeded, and later calls asked the working Sentinel first; with a hanging first startup node, the first call to a Redis Cluster took 10 s and succeeded. A Redis Cluster that cannot be connected is remembered for 5 s (see Redis clients): calls in that time get the same error at once, and the first call after it tries again.

If startup nodes can become unreachable without refusing connections (a firewall that drops packets), lower the timeouts of that cluster in options so that number of startup nodes × timeout stays below 15 s: with socket_timeout: 3 and socket_connect_timeout: 2, three hanging startup nodes failed with their own error after 9 s, and a hanging first startup node delayed the first call by 3 s. socket_timeout also limits every command on that cluster, so keep it above the slowest command you expect. Sentinels need no tuning for this: with the 2 s cap, even five Sentinels that do not answer fail after about 10 s.

Concurrency. Tool calls run in worker threads, 8 per cluster. Calls to a cluster beyond 8 at a time wait for a free worker; the wait counts toward their 15 s, and a call still waiting at the limit is dropped without running. When only one cluster is configured, a call that leaves out cluster runs on that cluster's workers, like a call that names it. Calls without a cluster (clusters_list without cluster, read_messages, unsubscribe, search_redis_documents) share another 8 workers; a read_messages call holds its worker for up to timeout_ms when fewer than max_messages arrive, so keep timeout_ms short when polling many subscriptions. A cluster that hangs ties up only its own workers. With 12 simultaneous get calls to a standalone cluster that accepted connections but never answered, 8 returned Timeout reading from socket after 10 s and the other 4 (which got a worker only after 10 s) the 15 s timeout message, while get calls to another cluster made at the same time answered within about 10 ms.

DNS lookups

Host names are resolved before the connection attempt starts, so the lookup is not covered by socket_connect_timeout. It runs for every new connection (the pool keeps and reuses them), for the names in hosts and for names that Sentinel or CLUSTER SLOTS return (usually IP addresses). A name that does not resolve fails quickly (Error -2 connecting to <host>:<port>. Name or service not known.), but a slow or unreachable DNS server can make a lookup take longer than the whole 15 s limit. In a pod, the default ndots:5 makes the resolver try a name with fewer than five dots, such as redis-1.example.com, with every search domain of the pod first (<namespace>.svc.cluster.local, svc.cluster.local, cluster.local, ...), each one a DNS query that has to fail before the real name is asked. For Redis hosts outside the cluster, write fully qualified names in hosts and lower ndots: deploy/openshift/deployment.yaml has a commented dnsConfig with ndots: "2", which makes names with two or more dots go to DNS as written first; short names such as a Service name still go through the search domains.

Transports and security

  • Streamable HTTP on /mcp (stateless, JSON responses). The server handles each request on its own and keeps no MCP session, so a pod restart does not break connected clients (pub/sub subscriptions excepted). The health endpoint GET /healthz returns {"status":"UP"}; it does not depend on Redis, so a cluster outage does not restart the pod. Use clusters_list to check the clusters.

  • stdio: JSON-RPC on stdin/stdout, with logs on stderr. There is no HTTP listener and no API key check (the config is still validated, so apiKey must not be the placeholder; use "" or delete the line).

Security:

  • API key (HTTP only): when an API key is set (apiKey, or --api-key / REDIS_MCP_API_KEY, which override it), every request to /mcp (and anything below it) must carry the header X-Authorization: <api-key> with exactly that value (compared in constant time). A missing or different value gets HTTP 401 with {"error": "unauthorized", "message": "X-Authorization header is missing or the API key does not match"}. The header is X-Authorization, not Authorization, and takes the raw key (no Bearer prefix). An empty or missing apiKey without a REDIS_MCP_API_KEY turns the check off. The startup log says API key authentication enabled for /mcp (header X-Authorization), or warns API key authentication DISABLED (apiKey is empty): anyone who can reach /mcp can call the tools. On OpenShift, the key comes from the Secret redis-mcp-api-key (see OpenShift deployment).

  • Browser requests are refused: a request to /mcp with an Origin header (that is, one sent by a web page) gets HTTP 403 {"error": "forbidden", "message": "Requests with an Origin header (from a web page) are not accepted"}, even with the right key. This stops a web page from driving the tools through the user's browser (CSRF, DNS rebinding). MCP clients such as Claude Code send no Origin header; browser-based clients cannot connect.

  • /healthz is not behind the key, so probes work. It shows no details.

  • Run HTTP behind TLS. The OpenShift Route terminates TLS at the edge.

  • On the Redis side, the server can do what its ACL user can do (see Redis ACL user per cluster). readOnly controls which tools are listed; it does not replace ACLs.

  • Keep secrets out of the repository. redis-mcp.yaml in the repository root, *.local.yaml (such as deploy/openshift/secret.local.yaml) and conf.d/ are git-ignored, and the image build context contains only src/, pyproject.toml, uv.lock, README.md and LICENSE. Examples use <...> placeholders; deploy/openshift/configmap.yaml holds no secrets, and deploy/openshift/secret.yaml only placeholders.

  • The pod does not mount a service account token (automountServiceAccountToken: false), because the server never calls the Kubernetes API.

JSON logs

Over HTTP, logs are JSON on stdout, one object per line with @timestamp, level, logger, message and any extra fields, at INFO (REDIS_MCP_LOG_LEVEL overrides it). The MCP SDK's own loggers are kept at WARNING and uvicorn's access log is off. Every tool call that passes the input schema validation produces one line with tool, cluster, durationMs, outcome (ok, error or cancelled) and error, for example:

{"@timestamp": "2026-10-20T06:25:24.482Z", "level": "INFO", "logger": "src.common.registry", "message": "tool call tool=set cluster=cbsbxrds durationMs=4 outcome=ok", "tool": "set", "cluster": "cbsbxrds", "durationMs": 4, "outcome": "ok", "error": null}
  • outcome is error when the tool raises or hits the 15 s limit. Most data tools report Redis errors (for example a missing ACL permission or Timeout reading from socket) in their result text instead, which is logged as ok. It is cancelled, with error: "cancelled by the client", when the client cancels the request before it finishes.

  • cluster is null for tools without a cluster, for clusters_list without one, and for calls that leave it out when only one cluster is configured.

  • Calls rejected before they reach the tool (a cluster outside the enum, a missing or mistyped parameter, an unknown tool) produce no tool call line.

  • A rejected HTTP request logs unauthorized request at WARNING with remoteAddr and headerPresent, never the key itself.

  • At startup the server logs <n> tools listed for clusters [...] (readOnly=..., disableDestructive=...) and a warning for each cluster without a password.

Over stdio, stdout carries the JSON-RPC stream, so logs go to stderr as plain [LEVEL] message lines, at WARNING by default; set REDIS_MCP_LOG_LEVEL=INFO to see the tool call lines. Inside a Python process that already has log handlers (pytest, for example), the server adds none of its own.

Container image

Dockerfile builds on python:3.14-slim and installs the locked dependencies into /app/.venv with uv sync --locked --no-dev. It is built to run under the OpenShift restricted-v2 SCC:

  • It runs as non-root user 1001. /app and /etc/redis-mcp are group 0 with g=u, so a random UID in group 0 works.

  • It writes nothing at runtime: the bytecode is compiled during the build and uv is not used to start the server, so the root filesystem can be read-only, with an emptyDir mounted at /tmp. (uv run inside the container would fail as a non-root user: Failed to initialize cache at /.cache/uv.)

  • CMD ["/app/.venv/bin/python", "src/main.py"]: with REDIS_MCP_CONFIG (or REDIS_MCP_CONFIG_DIR) set it runs the multi-cluster mode, otherwise the single-cluster mode from the REDIS_* variables over stdio. The OpenShift Deployment runs /app/.venv/bin/redis-mcp --config /etc/redis-mcp/config/redis-mcp.yaml instead. /app/.venv/bin is on PATH, so redis-mcp also works as a command. Port 8080 is exposed; keep port: 8080 in the config to get HTTP.

  • Mount points: /etc/redis-mcp/config (redis-mcp.yaml), /etc/redis-mcp/passwords (<cluster>_pass files), /etc/redis-mcp/tls (CA certificates).

The image works with --user 12345:0 --read-only --tmpfs /tmp (a random UID in group 0 and a read-only root filesystem, as on OpenShift), in both the multi-cluster (HTTP) and the single-cluster (stdio) mode.

Published image

.github/workflows/docker-push.yml builds the image and pushes it to Docker Hub as chosenwar/redis-mcp:

Trigger

Tags

push to main

latest, sha-<short commit>

tag vX.Y.Z

X.Y.Z, sha-<short commit>

manual run (Actions → Docker push → Run workflow)

sha-<short commit> (plus latest on main)

Before pushing, the workflow smoke-tests the image the way OpenShift runs it (--user 12345:0 --read-only --tmpfs /tmp): redis-mcp --help, the import, /healthz, 401 without the API key and tools/list with it. It needs the repository secrets DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (a Docker Hub access token with Read & Write scope). To release a version, set it in pyproject.toml and src/version.py, then tag it: git tag v0.1.0 && git push origin v0.1.0.

OpenShift deployment

deploy/openshift/ is a kustomize directory:

File

Content

configmap.yaml

The ConfigMap redis-mcp-config: the config under the key redis-mcp.yaml, with the settings and the example clusters of Configuration (cbdevrds, cbtstrds, the Redis Cluster cbprdrds, and cbsbxrds with readOnly: false) but no apiKey line. Set the clusters for your environment; it holds no secrets.

deployment.yaml

1 replica (Recreate: pub/sub subscriptions and the client caches live in the process). Startup, liveness and readiness probes on /healthz. A restricted-v2 security context (non-root, no privilege escalation, all capabilities dropped, RuntimeDefault seccomp, read-only root filesystem). No service account token is mounted. A /tmp emptyDir (64Mi). Requests of 50m CPU and 128Mi memory, with a 256Mi memory limit (the server uses under 100 MB at idle). The command is /app/.venv/bin/redis-mcp --config /etc/redis-mcp/config/redis-mcp.yaml. The config is mounted at /etc/redis-mcp/config, the passwords (Secret redis-mcp-passwords) at /etc/redis-mcp/passwords (mode 0440). REDIS_MCP_API_KEY comes from the key api-key of the Secret redis-mcp-api-key; the entry is active and required, so the pod does not start until that Secret exists (CreateContainerConfigError). An optional CA Secret mount at /etc/redis-mcp/tls and a dnsConfig with ndots: "2" (see DNS lookups) are commented out.

service.yaml

redis-mcp Service on port 8080 (http).

route.yaml

Edge TLS (HTTP redirects to HTTPS), haproxy.router.openshift.io/timeout: 60s.

secret.yaml

The Secrets redis-mcp-passwords (one <cluster>_pass key per cluster of configmap.yaml, plus commented keys for a Sentinel cluster) and redis-mcp-api-key (key api-key), with "<password>"/"<api-key>" placeholders. Not part of the kustomization: copy it to secret.local.yaml (git-ignored), fill it in and oc apply -f it.

kustomization.yaml

configmap.yaml, deployment.yaml, service.yaml and route.yaml (not secret.yaml), labels, and the images: entry that sets the image.

oc apply -k deploy/openshift creates the ConfigMap redis-mcp-config with the Deployment, the Service and the Route. The two Secrets hold the passwords and the API key, so the repository has only the template secret.yaml: you apply a filled-in copy with oc apply -f.

0. Select the project the server runs in. The commands below use the current project:

oc new-project redis-mcp      # or switch to an existing one: oc project <namespace>

1. Pick the image. By default kustomization.yaml uses the published docker.io/chosenwar/redis-mcp:latest (see Container image); pin a release tag (X.Y.Z) for production. To build your own instead, push it to a registry the cluster can pull from:

docker build -t <registry>/<namespace>/redis-mcp:<tag> .     # or podman build
docker push <registry>/<namespace>/redis-mcp:<tag>

or build it inside the cluster, into the internal registry (the build pod needs access to Docker Hub and PyPI):

oc new-build --name redis-mcp --binary --strategy docker
oc start-build redis-mcp --from-repo . --follow   # -> image-registry.openshift-image-registry.svc:5000/<namespace>/redis-mcp:latest

--from-repo uploads the committed files of the clone, so local config and password files are not sent to the cluster.

2. Set the image in deploy/openshift/kustomization.yaml (only needed for a release tag or your own image):

images:
  - name: redis-mcp
    newName: docker.io/chosenwar/redis-mcp   # or image-registry.openshift-image-registry.svc:5000/<namespace>/redis-mcp
    newTag: latest                           # or a release, e.g. 0.1.0

3. Set the clusters in deploy/openshift/configmap.yaml (keys: Configuration). Keep port: 8080 and passwordsDir: /etc/redis-mcp/passwords, which match the Deployment, and leave apiKey out: the key comes from the Secret (REDIS_MCP_API_KEY overrides apiKey, but the config is checked first, so an apiKey: <api-key> placeholder would still stop the pod). oc apply -k writes this file over an existing redis-mcp-config ConfigMap, also one made earlier with oc create configmap.

4. Fill in the Secrets. Copy the template to a git-ignored file, replace every <...> and apply it:

cp deploy/openshift/secret.yaml deploy/openshift/secret.local.yaml   # git-ignored (*.local.yaml)
openssl rand -hex 32                                                 # a value for api-key
# in secret.local.yaml: one <cluster>_pass per cluster of configmap.yaml, and api-key
oc apply -f deploy/openshift/secret.local.yaml
oc extract secret/redis-mcp-api-key --keys=api-key --to=-            # shows the key again later
  • redis-mcp-passwords needs one <cluster>_pass key per cluster (see Passwords, also for quoting the values). A cluster without a key connects without AUTH, with a warning in the log.

  • api-key is required: while the Secret redis-mcp-api-key does not exist, the pod does not start (CreateContainerConfigError), and while api-key is still the <api-key> placeholder or starts or ends with whitespace, the server stops (redis-mcp: --api-key / REDIS_MCP_API_KEY: apiKey ...).

  • An empty api-key: "" is ignored, and with no apiKey in the ConfigMap authentication is then off (API key authentication DISABLED in the log).

5. Apply the manifests (the ConfigMap, the Deployment, the Service and the Route):

oc apply -k deploy/openshift
oc rollout status deployment/redis-mcp
oc get route redis-mcp

Applied before the Secrets, the pod waits (CreateContainerConfigError, or ContainerCreating with a failed mount of redis-mcp-passwords) and starts once they exist.

For clusters with TLS and a private CA, create a Secret with the CA, uncomment the tls-ca volume and its mount in deployment.yaml, and set tls: {caFile: /etc/redis-mcp/tls/ca.crt} on those clusters:

oc create secret generic redis-mcp-tls --from-file=ca.crt=/path/to/ca.crt

The Route has no fixed host, so OpenShift generates redis-mcp-<namespace>.<apps-domain>. To use redis-mcp.<apps-domain>, uncomment spec.host in route.yaml. The 60 s router timeout leaves room above the 15 s tool limit.

Smoke test:

curl -s https://redis-mcp.<apps-domain>/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "X-Authorization: $REDIS_MCP_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"clusters_list","arguments":{}}}'

Updating the config, the passwords or the API key. They are read only at startup, so edit deploy/openshift/configmap.yaml or deploy/openshift/secret.local.yaml, apply it again and restart the pod:

oc apply -k deploy/openshift                         # after a change to configmap.yaml
oc apply -f deploy/openshift/secret.local.yaml       # after a change to the passwords or api-key
oc rollout restart deployment/redis-mcp

A new api-key (openssl rand -hex 32) means the clients need the new key too. A key deleted from stringData stays in the Secret after oc apply; oc replace -f deploy/openshift/secret.local.yaml removes it.

With Recreate, the old pod stops before the new one starts, so there is a short gap, and open pub/sub subscriptions are lost.

Claude Code

Add the server to .mcp.json (project scope). The key stays in an environment variable and never in the file:

{
  "mcpServers": {
    "redis": {
      "type": "http",
      "url": "https://redis-mcp.<apps-domain>/mcp",
      "headers": {
        "X-Authorization": "${REDIS_MCP_KEY}"
      }
    }
  }
}
export REDIS_MCP_KEY=<api-key>
claude

If the Route certificate is signed by a corporate CA, Node.js (which Claude Code runs on) must trust that CA. Point NODE_EXTRA_CA_CERTS to the CA bundle in PEM format before you start claude:

export NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem

Run /mcp inside Claude Code to check the connection. The tools appear as mcp__redis__<tool>.

stdio alternative. Claude Code can also start the server locally. The clusters must then be reachable from your machine, and apiKey is not used (but still checked: "" or no line, not the placeholder). Pass --transport stdio explicitly, because a config with port set would otherwise start HTTP. With the redis-mcp command from a clone (after uv sync), the passwords come from the environment:

{
  "mcpServers": {
    "redis": {
      "type": "stdio",
      "command": "/path/to/redis-mcp/.venv/bin/redis-mcp",
      "args": ["--config", "/path/to/redis-mcp.local.yaml", "--transport", "stdio"],
      "env": {
        "cbsbxrds_pass": "${CBSBXRDS_PASS}"
      }
    }
  }
}

The same with the container image (absolute host paths; see With Docker for the mounts and file permissions):

{
  "mcpServers": {
    "redis": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", "--rm", "-i", "--read-only", "--tmpfs", "/tmp",
        "-v", "/path/to/redis-mcp.local.yaml:/etc/redis-mcp/config/redis-mcp.yaml:ro",
        "-v", "/path/to/redis-mcp-passwords:/etc/redis-mcp/passwords:ro",
        "chosenwar/redis-mcp:latest",
        "/app/.venv/bin/redis-mcp", "--config", "/etc/redis-mcp/config/redis-mcp.yaml", "--transport", "stdio"
      ]
    }
  }
}

Single-cluster mode

Without --config and --config-dir (and without REDIS_MCP_CONFIG/REDIS_MCP_CONFIG_DIR), the server serves one Redis described by the connection flags or the REDIS_* environment variables, as a cluster named default:

  • every toolset is listed, json and search included, and nothing is hidden by readOnly or disableDestructive;

  • the tools' cluster parameter is optional (see Which tools are listed);

  • the transport is stdio, unless --transport http (port 8080) or --http-port is given; there is no API key unless --api-key / REDIS_MCP_API_KEY sets one.

uv run redis-mcp --url redis://localhost:6379/0                     # stdio
REDIS_PWD='<password>' uv run redis-mcp --host redis.example.com --port 6380 --username mcp-reader --ssl
REDIS_HOST=localhost REDIS_PORT=6379 uv run python src/main.py      # environment only (the image's default command)
REDIS_MCP_API_KEY='<api-key>' uv run redis-mcp --url redis://localhost:6379/0 --transport http   # HTTP on :8080

Variable

Flag

Default

Meaning

--url

not set

redis://[user:password@]host:port/db, or rediss://... for TLS. Query parameters: ssl_cert_reqs, ssl_ca_certs, ssl_ca_path, ssl_certfile, ssl_keyfile, db. Another scheme stops with Error parsing Redis URI: ... (exit code 1).

REDIS_HOST

--host

127.0.0.1

Host name or IP address.

REDIS_PORT

--port

6379

Port.

REDIS_DB

--db

0

Database number (not used with cluster mode).

REDIS_USERNAME

--username

not set

ACL user; without it the password authenticates the default user.

REDIS_PWD

--password

not set

Password. Prefer the variable: a command line can be read with ps.

REDIS_SSL

--ssl

off

TLS (true, 1 or t).

REDIS_SSL_CA_CERTS

--ssl-ca-certs

not set

CA bundle file (tls.caFile).

REDIS_SSL_CA_PATH

--ssl-ca-path

not set

CA directory (tls.caPath). Not supported with cluster mode.

REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE

--ssl-certfile, --ssl-keyfile

not set

Client certificate and key (mutual TLS).

REDIS_SSL_CERT_REQS

--ssl-cert-reqs

required

required, optional or none.

REDIS_CLUSTER_MODE

--cluster-mode

off

Redis Cluster (true, 1 or t); the host is then a startup node.

  • With the redis-mcp command, the flags that have a default (--host, --port, --db, --ssl, --ssl-cert-reqs, --cluster-mode) win over their variables even when they are not given, so REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_SSL, REDIS_SSL_CERT_REQS and REDIS_CLUSTER_MODE apply only to python src/main.py. The other variables apply to both, unless the flag is given.

  • --cluster-mode together with --ssl and --ssl-ca-path (or the variables) stops with redis-mcp: --ssl-ca-path / REDIS_SSL_CA_PATH is not supported with --cluster-mode (redis-py's RedisCluster ignores it); use --ssl-ca-certs / REDIS_SSL_CA_CERTS (exit code 2), because the nodes would be verified against the system CAs only. Use a CA bundle file instead.

  • Over HTTP the logs are JSON as described in JSON logs; over stdio they are plain lines on stderr.

Entra ID

For Azure Managed Redis, --entraid-auth-flow / REDIS_ENTRAID_AUTH_FLOW replaces the password with a Microsoft Entra ID token (redis-entraid, renewed before it expires); the username and password are then not used.

Flow

Settings

service_principal

REDIS_ENTRAID_CLIENT_ID, REDIS_ENTRAID_CLIENT_SECRET, REDIS_ENTRAID_TENANT_ID

managed_identity

REDIS_ENTRAID_IDENTITY_TYPE: system_assigned (default), or user_assigned with REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID; REDIS_ENTRAID_RESOURCE (default https://redis.azure.com/)

default_credential

REDIS_ENTRAID_SCOPES (comma-separated, default https://redis.azure.com/.default)

The --entraid-* flags set the same values (redis-mcp --help; a user-assigned identity takes --entraid-client-id). As with the connection flags, the ones with a default (identity type, scopes, resource, token refresh and retry settings) win over their variables with the redis-mcp command. With a config file, the same settings apply to every cluster with entraid: true.

Troubleshooting

Start with clusters_list: it shows every cluster's mode, hosts, readOnly, modules and auth, and connects to each one to report status (ok, error or timeout), latencyMs, redisVersion, role and, for a Redis Cluster, primaries, or the error.

Symptom

Cause and fix

redis-mcp: Invalid config: then - apiKey: apiKey still holds a placeholder ...

apiKey was not filled in. Set a real key (openssl rand -hex 32), "" to turn authentication off, or delete the line when the key comes from REDIS_MCP_API_KEY (the config is checked even then).

redis-mcp: --api-key / REDIS_MCP_API_KEY is blank; ...

The key from --api-key or REDIS_MCP_API_KEY (on OpenShift: api-key of the Secret redis-mcp-api-key) holds only whitespace. Set a key, or leave the value empty or unset to keep apiKey from the config. On OpenShift, fix api-key in secret.local.yaml, oc apply -f it and run oc rollout restart deployment/redis-mcp.

redis-mcp: --api-key / REDIS_MCP_API_KEY: apiKey ...

The key from --api-key or REDIS_MCP_API_KEY is a <...>/${...} placeholder (on OpenShift: api-key in secret.local.yaml was not filled in), or starts or ends with whitespace, such as a \n or a space inside the quotes of the stringData value, or the final line break of a file given to oc create secret --from-file. Set the key (openssl rand -hex 32), apply the Secret again and restart the deployment.

Pod in CreateContainerConfigError (secret "redis-mcp-api-key" not found), or in ContainerCreating with MountVolume.SetUp failed for volume "passwords" : secret "redis-mcp-passwords" not found

The Secrets do not exist in the pod's namespace: oc apply -f deploy/openshift/secret.local.yaml in that project (see OpenShift deployment). The pod starts once they exist.

redis-mcp: Invalid config: then - clusters[0]...

Fix the named key, for example clusters[0]: a standalone cluster takes exactly one host:port in hosts, clusters[0]: masterName is required when mode is sentinel, clusters[0].hosts: hosts entry 1 is not host:port (or [IPv6 address]:port) or clusters[0].password: Extra inputs are not permitted (passwords go into <name>_pass).

clusters[0]: options <keys> not supported in <mode> mode (supported: ...)

options only takes the keys of Cluster options for the cluster's mode. Addresses, db, username and tls have their own keys; passwords go into <name>_pass.

clusters[0]: options.<key> must be <rule>

The option's value breaks the rule in the "Value" column of Cluster options, for example options.socket_timeout must be a number of seconds > 0 and <= 3600 for 0, null or 1e10, or options.client_name must be a name without spaces.

clusters[0]: tls.caPath is not supported in cluster mode; use caFile

Put the CA certificates into one PEM bundle and set caFile.

redis-mcp: --ssl-ca-path / REDIS_SSL_CA_PATH is not supported with --cluster-mode ...

The single-cluster mode's equivalent of the row above: use --ssl-ca-certs / REDIS_SSL_CA_CERTS with a CA bundle file instead of the CA directory.

clusters[0].hosts: hosts takes host:port entries without a scheme or credentials ...

hosts contains :// or @, that is a URL or user:password@. Write plain host:port entries, put the user into username and the password into <name>_pass.

clusters[0].hosts: hosts entry <n> is not host:port (or [IPv6 address]:port) or hosts entry <n>: the port must be between 1 and 65535

Entry <n> of hosts (1 for the first) is empty (for example after a trailing comma), has no port (or one not written with the digits 0-9), has a space inside the host, or its port is outside 1–65535. Write IPv6 addresses in brackets and quote them in YAML ("[2001:db8::1]:6379").

clusters: cluster name <x>_sentinel clashes with the Sentinel password of cluster <x> ...

Both clusters would read the password file <x>_sentinel_pass. Rename one of them.

redis-mcp: clusters <names> set entraid: true, but no Entra ID auth flow is configured ...

Set --entraid-auth-flow / REDIS_ENTRAID_AUTH_FLOW and its settings (see Entra ID), or remove entraid: true.

redis-mcp: Could not read config file: <path> (not UTF-8: invalid byte on line <n>) or Could not read password file <path> (not UTF-8)

Save the file as UTF-8 (a byte order mark is accepted in config files). The config message names the line of the first byte that is not UTF-8.

redis-mcp: Could not parse config file: <path> (... line <l>, column <c> ...)

The file is not valid YAML; fix it at the line and column named. The message does not quote the line.

redis-mcp: Unknown toolset: [...]

Valid toolsets are core, data, json, search, pubsub and docs.

redis-mcp: --host cannot be combined with --config/--config-dir; ...

The connection flags belong to the single-cluster mode; define the clusters in the config file.

HTTP 401 X-Authorization header is missing or the API key does not match

The key does not match the server's key exactly. Check that REDIS_MCP_KEY is exported in the shell that starts Claude Code and that it equals api-key in the Secret redis-mcp-api-key (oc extract secret/redis-mcp-api-key --keys=api-key --to=-; after a change to the Secret, the pod needs oc rollout restart deployment/redis-mcp): no extra whitespace, no Bearer prefix, header name X-Authorization. The server log shows unauthorized request with headerPresent.

HTTP 403 Requests with an Origin header (from a web page) are not accepted

The request came from a web page or a browser-based client. Use a non-browser MCP client such as Claude Code.

Startup warning cluster <name>: no password (...); connecting without AUTH, then Authentication required.

The Secret has no <name>_pass key for this cluster (the name is case-sensitive and must match name exactly), or the Secret is not mounted at passwordsDir. On OpenShift, add the key to redis-mcp-passwords in secret.local.yaml, oc apply -f it and restart the deployment.

invalid username-password pair or user is disabled.

Wrong password or username, or the ACL user is off. On OpenShift, check the value with oc extract secret/redis-mcp-passwords --keys=<name>_pass --to=-: inside double quotes YAML reads \ as an escape, so a backslash is written \\ (see Passwords).

this user has no permissions to run the '<command>' command

The cluster's ACL user lacks the command. Expected for writes with mcp-reader; otherwise add the rule (see Redis ACL user per cluster).

Redis Cluster cannot be connected. Please provide at least one reachable node: Cluster mode is not enabled on this node

Either the cluster is not a Redis Cluster (use mode: standalone or sentinel), or its ACL user lacks +cluster|slots.

Results like Error retrieving key a: 15495 127.0.0.1:17002 (a slot and an address: a MOVED redirect)

The cluster is a Redis Cluster but configured with mode: standalone. Set mode: cluster.

Error 111 connecting to <host>:<port>. Connection refused. or Timeout connecting to server

Check hosts and the network path from the pod (egress, firewall). The connect timeout is 5 s; raise it with options: {socket_connect_timeout: ...} if needed.

Error -2 connecting to <host>:<port>. Name or service not known. (or Error -3 ... Temporary failure in name resolution)

The host name does not resolve from the pod. Check the name in hosts and the pod's DNS (see DNS lookups).

No master found for '<masterName>'

Without anything after it: the Sentinels answered, but none of them knows a healthy primary named masterName; check masterName. With : <host>:<port> - <error type>: <message>, ... after it: those Sentinels could not be asked. ConnectionError or TimeoutError: check the Sentinel addresses (usually port 26379) and the network path (each Sentinel gets at most 2 s). AuthenticationError: Authentication required. or invalid username-password pair ...: set or fix <name>_sentinel_pass.

Timeout connecting to server after about 10 s on a cluster with tls, or TLS errors such as CERTIFICATE_VERIFY_FAILED

tls set on a port without TLS: the TLS handshake gets no answer. For certificate errors, set caFile to the CA that signed the server certificate (mounted at /etc/redis-mcp/tls), and use the host name the certificate is issued for in hosts.

Tool did not finish within 15 s (timeout). The cluster may be slow or unreachable (check clusters_list), or the request too large (narrow the pattern or count).

Run clusters_list. If the cluster shows status: timeout or an error, the cluster or the path to it is the cause: Redis Cluster startup nodes that do not answer cost one timeout each and can add up to more than 15 s, and so can more than 8 simultaneous calls to a cluster that hangs, or a slow DNS lookup (see Time limits, retries and concurrency); fix hosts, the network or DNS, or lower the timeouts in options. If the cluster is fine, the request is too large: scan_all_keys walks the whole keyspace in one call, whatever the pattern. On large standalone or Sentinel databases use scan_keys and page through the keys with the cursor it returns, or use a more specific tool; on a Redis Cluster scan_keys cannot page (see Toolsets), so narrow the pattern.

Error: scan_keys cannot continue a cursor on a Redis Cluster ...

Every primary of a Redis Cluster has its own cursor. Call scan_keys with cursor 0, or use scan_all_keys with a narrow pattern.

status: timeout in clusters_list

The cluster's check did not finish within 13 s, for example two or more Redis Cluster startup nodes that accept connections but do not answer, or a slow DNS lookup. See the Tool did not finish within 15 s row.

Redis Cluster cannot be connected ... again at once, right after the cluster was fixed

A failed Redis Cluster connection is remembered for 5 s (see Redis clients). The first call after that tries again.

A call fails once with Error while reading from <host>:<port> : (104, 'Connection reset by peer'), Connection closed by server. or Timeout reading from socket, and the next call works

The connection broke while the command was in flight, typically during a restart or a failover. The server sends no command twice, so a write in such a call may or may not have been applied: check before you repeat it.

Too many connections

More calls ran on the cluster at once than its tools' client has connections, usually because options sets max_connections below the 8 calls a cluster runs at a time. Remove the option (default 16) or raise it. Pub/sub subscriptions do not count: they have a client of their own.

A tool or cluster is missing from the list, or Unknown tool: <name>

Its toolset is not in toolsets, the cluster is read-only (write tools), disableDestructive is on (delete/pop/remove tools), or the cluster does not list the module (json/search tools).

Input should be '<cluster>'

The tool is not available on the named cluster (for example a write tool on a read-only cluster); the error lists the allowed values.

Subscription '<id>' was not found

The pod restarted (subscriptions live in the process) or the subscription was closed. Subscribe again.

Development

src/
  main.py                    command line (click), single-cluster mode, transport selection
  version.py                 version
  common/
    config_loader.py         YAML loading, --config-dir merge, validation, passwords
    config.py                REDIS_* and REDIS_ENTRAID_* variables of the single-cluster mode, Redis URL parsing
    connection.py            per-cluster client cache and client arguments (standalone, Redis Cluster, Sentinel)
    registry.py              toolsets, tool kinds, tools per config, cluster enum, time limit, worker pools
    server.py                FastMCP server, /healthz
    http.py                  Streamable HTTP: API key and Origin checks, uvicorn
    logging_utils.py         JSON logs (HTTP), stderr logs (stdio)
    entraid_auth.py          Entra ID credential providers
    subscription_manager.py  pub/sub subscriptions
  tools/                     the tools, one module per area (string, hash, list, set, sorted_set, stream, json, ...)
tests/                       pytest suite; redis_servers.py starts throwaway redis-server processes
deploy/openshift/            kustomize manifests and templates

Command

What it does

uv sync

Creates .venv with the dependencies and the dev tools (pytest, ruff, bandit, ...)

uv run pytest

All tests, with coverage (fails under 80 %; reports in htmlcov/ and coverage.xml)

uv run pytest --no-cov tests/test_config_loader.py

One test file, without coverage

uv run ruff check src/ tests/

Lint

uv run ruff format --check src/ tests/

Format check (uv run ruff format src/ tests/ fixes it)

uv run bandit -r src/

Security scan

docker build -t redis-mcp:latest .

Container image

  • The suite has over 1,500 tests. tests/test_connection.py and tests/test_multi_cluster_integration.py start their own redis-server processes on free ports (tests/redis_servers.py), and a Redis Cluster of three primaries built with redis-cli, when those binaries are installed; without them these tests are skipped. tests/test_integration.py starts the server in the single-cluster mode against the Redis at REDIS_HOST/REDIS_PORT (default localhost:6379); its Redis tests are skipped when nothing answers there.

  • CI (.github/workflows/ci.yml) runs ruff and bandit, runs the tests on Python 3.10 to 3.14 (Ubuntu with redis-server and redis-tools installed, plus Windows and macOS without Redis), builds the wheel and checks that redis-mcp --help runs from it, and builds the image.

  • To add a tool, write a function in a module of src/tools/ and decorate it with @redis_tool("<toolset>"). Pass ToolKind.WRITE or ToolKind.DESTRUCTIVE as the second argument for a tool that changes data, requires="json" or requires="search" for a module tool, and cluster="optional" or cluster="none" for a tool that is not tied to one cluster. The function name becomes the tool name and the docstring its description. The cluster parameter, the WRITE: prefix, the annotations, the time limit and the tool call log line are added by the registry, and the modules of src/tools/ are imported automatically. Inside the tool, RedisConnectionManager.get_connection() returns the client of the call's cluster. Then add the tool to the Tool reference.

License

MIT, see LICENSE. redis-mcp started from redis/mcp-redis; LICENSE keeps its copyright notice.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    Provides access to Redis databases. This server enables LLMs to interact with Redis key-value stores through a set of standardized tools.
    148 npm
    30
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants to perform comprehensive Redis database operations including managing strings, hashes, lists, sets, sorted sets, TTL management, and data backup/restore. Supports secure connections and provides batch operations for efficient Redis interaction through natural language.
    34
    2 npm
    2
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    RedisNexus is an AI-powered operations intelligence platform that provides 16 enterprise-grade tools for managing Redis data, health monitoring, and cache strategy. It enables AI assistants to perform secure, multi-tenant Redis operations and real-time intelligence analysis within production-ready Kubernetes environments.
    -
  • A
    license
    A
    quality
    A
    maintenance
    Enables exploring and diagnosing a Redis instance from MCP clients with read-only safety, using SCAN instead of KEYS for safe key enumeration.
    7
    200 npm
    2
    MIT