redis-mcp
Provides tools for interacting with Redis fleets, including standalone Redis servers, Redis Cluster, and Sentinel-managed primaries. Offers 54 tools across core, data, JSON, search, pubsub, and docs toolsets, with read-only and destructive-operation controls, per-cluster isolation, and bounded tool calls.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@redis-mcpshow me memory usage across all my Redis clusters"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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
clusterparameter 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: truehides every write tool (one sandbox cluster opts back in), anddisableDestructive: truehides 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).hostsandoptionscannot 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/healthzendpoint, a non-root image on Docker Hub that runs on a read-only root filesystem, and kustomize manifests for OpenShift (restricted-v2SCC).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.yamlThe 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 keyTo run over stdio instead (for a local MCP client that starts the process itself):
uv run redis-mcp --config redis-mcp.local.yaml --transport stdioWith 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 |
|
passwords |
|
CA certificates (optional) |
|
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 stdioNotes:
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.yamlinstead of the command. Without that variable it runs the single-cluster mode from theREDIS_*variables.To keep the API key out of the mounted file, delete
apiKeythere 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
600owned by your user are not readable by UID 1001; run the container as your UID with--user "$(id -u):0".Inside the container
localhostis the container itself. To reach a Redis on the host, use--network host(Linux; then-pis not needed) or the host's address.With Podman, use the same commands with
podman; on SELinux hosts add:Zto 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>/mcpClients 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 |
|
| Main config file (YAML). |
|
| Directory of drop-in |
| Transport. Defaults to | |
| HTTP port (1–65535). Overrides | |
|
| API key that HTTP clients must send in |
| 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 |
| Log level: |
| Cluster passwords, when there is no such file in |
| Endpoint of the |
| Entra ID settings for clusters with |
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-readerTop-level keys
Key | Type | Default | Meaning |
| integer (0–65535) | not set | HTTP port. When it is set (> 0) and |
| string | not set | Static API key. HTTP requests to |
| boolean |
|
|
| boolean |
|
|
| list of strings |
| Toolsets to enable, which also sets the order of |
| path |
| Directory of the |
| 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 |
| string | (required) | Unique name of 1–63 characters from |
|
|
|
|
| string or list | (required) |
|
| integer (≥ 0) |
| Database number ( |
| string | not set | ACL user (redis-py |
| string | not set | Name of the monitored primary. Required with |
|
| not set (no TLS) | TLS for the connections to Redis (redis-py |
| boolean | global | Per-cluster override of |
| list of |
| Modules the cluster has. The |
| boolean |
| Authenticate with the Entra ID credential provider (settings from the |
| 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 |
| all |
| a number of seconds > 0 and <= 3600 | Seconds to wait for a reply. With |
| all |
| a number of seconds > 0 and <= 3600 | Seconds to wait for a connection. With |
| all | off |
| TCP keepalive on the connections to Redis. |
| all |
| an integer >= 1 | Size of the connection pool of the tools' client (with |
| all | not set | a name without spaces (not empty) | Connection name set with |
| all |
|
| RESP protocol version. |
|
|
| a number of seconds >= 0 | A connection that was idle for longer than this many seconds is checked with |
|
|
|
| Send read commands to the replicas as well (round robin); replicas can return slightly older data. redis-py 6 marks the argument as deprecated. |
|
|
|
|
|
|
|
| an integer >= 0 | Number of |
|
|
|
| After connecting, use the discovered nodes as startup nodes for later slot map refreshes. Set |
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 |
| path | not set |
| CA bundle (PEM) trusted in addition to the system CAs. |
| path | not set |
| Directory of CA certificates. Not supported with |
| path | not set |
| Client certificate (mutual TLS). |
| path | not set |
| Key of the client certificate. |
|
|
|
| Verification of the server certificate. |
| boolean | not set (redis-py default: on) |
| Match the server certificate against the host name in |
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=Falseforget_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 theclusters_listchecks. Pub/sub subscriptions hold their connection untilunsubscribe, so they use a client of their own with up to 50 connections (withmode: 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, itstlssettings, one attempt per command and the password<name>_sentinel_pass(no username). The otheroptionsapply 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 exampleNo 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
*.yamland*.ymlfiles 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--configone after another.--config-dircan also be used on its own.Maps are merged deeply.
clustersis merged byname: an entry with an existing name overrides only the fields it sets (itstlsandoptionsmaps are merged key by key), and an entry with a new name is appended. Every other list, such astoolsets,modulesor ahostslist, 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:
the file
<passwordsDir>/<name>_pass(for example/etc/redis-mcp/passwords/cbdevrds_pass) if it exists,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 AUTHThe 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 runsThe 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|infoand, 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|infoThe rules are applied from left to right.
-@dangerousmust come before+info +client|list:INFOandCLIENT LIST(used by theinfo,client_listandclusters_listtools) are in the@dangerouscategory, so a-@dangerousat the end would remove them again.-@dangerousalso removesKEYS,FLUSHALL,FLUSHDB,CONFIG,SHUTDOWNandCLIENT KILL.+cluster|slotsis needed on a Redis Cluster: redis-py reads the slot map when it connects, and without this command the connection fails withCluster mode is not enabled on this node.+cluster|nodes +cluster|infoletcluster_nodesread 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-readercan runget,hgetall,scan_keys,scan_all_keys,type,dbsize,info,client_listandclusters_list, andset/deletefail withthis user has no permissions to run the 'set' command;mcp-writercan also runset,delete,expire,renameand the stream group commands, but notFLUSHALL,FLUSHDB,KEYS,CONFIGorSHUTDOWN.The
pubsubtoolset needs+subscribe +psubscribe +unsubscribe +punsubscribe(and+publishfor the write toolpublish): pub/sub commands are not in@reador@write.The
jsonandsearchtools 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 withACL DRYRUN mcp-reader JSON.GET somekeyand 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 anaclfile) orCONFIG REWRITE.Set
username: mcp-reader(ormcp-writer) on the cluster in the config, and put the password in<name>_pass.
Toolsets
Toolset | Tools | What it covers | Available on |
| 11 | The cluster list with a connection check, the nodes of a cluster ( | Every cluster. |
| 29 | Strings, hashes (including vectors stored as binary fields), lists, sets, sorted sets, and streams with consumer groups | Every cluster |
| 3 | RedisJSON documents: get, set and delete at a path | Clusters with |
| 6 | Redis Query Engine: indexes, index information, indexed key count, vector index creation on hashes, vector (KNN) and hybrid search | Clusters with |
| 5 | Publish, subscribe to a channel or a pattern, read the queued messages, unsubscribe | Every cluster. |
| 1 | Search the redis.io documentation | Not tied to a cluster (no |
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:
Its toolset must be in
toolsets.jsonandsearchtools need a cluster that lists the module inmodules.Write tools (descriptions starting with
WRITE:) need the cluster's effectivereadOnlyto befalse. The cluster's ownreadOnlyapplies if it is set, otherwise the global one does.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 |
|
|
|
|
|
| not listed |
| not listed (toolsets |
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 |
11 | 8 | 2 | 1 | |
29 | 10 | 11 | 8 | |
3 | 1 | 1 | 1 | |
6 | 5 | 1 | 0 | |
5 | 4 | 1 | 0 | |
1 | 1 | 0 | 0 | |
Total | 55 | 29 | 16 | 10 |
core (11 tools)
Tool | Kind | Needs | Description |
| read |
| 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 |
| 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) |
| destructive | — | Delete a Redis key |
| read | — | Returns the string representation of the type of the value stored at key |
| write | — | Set an expiration time for a Redis key |
| write | — | Renames a Redis key from old_key to new_key |
| read | — | Scan keys in the Redis database using the SCAN command (non-blocking, production-safe) |
| read | — | Scan and return ALL keys matching a pattern using multiple SCAN iterations |
| read | — | Get the number of keys stored in the Redis database |
| read | — | Get Redis server information and statistics |
| read | — | Get a list of connected clients to the Redis server |
data (29 tools)
Tool | Kind | Needs | Description |
| write | — | Set a field in a hash stored at key with an optional expiration time |
| read | — | Get the value of a field in a Redis hash |
| destructive | — | Delete a field from a Redis hash |
| read | — | Get all fields and values from a Redis hash |
| read | — | Check if a field exists in a Redis hash |
| write | — | Store a vector as a field in a Redis hash |
| read | — | Retrieve a vector from a Redis hash and convert it back from binary blob |
| write | — | Push a value onto the left of a Redis list and optionally set an expiration time |
| write | — | Push a value onto the right of a Redis list and optionally set an expiration time |
| destructive | — | Remove and return the first element from a Redis list |
| destructive | — | Remove and return the last element from a Redis list |
| read | — | Get elements from a Redis list within a specific range |
| read | — | Get the length of a Redis list |
| destructive | — | Remove elements from a Redis list |
| write | — | Add a value to a Redis set with an optional expiration time |
| destructive | — | Remove a value from a Redis set |
| read | — | Get all members of a Redis set |
| write | — | Add a member to a Redis sorted set with an optional expiration time |
| read | — | Retrieve a range of members from a Redis sorted set |
| destructive | — | Remove a member from a Redis sorted set |
| write | — | Add an entry to a Redis stream with an optional expiration time |
| read | — | Read entries from a Redis stream |
| destructive | — | Delete an entry from a Redis stream |
| write | — | Create a consumer group for a Redis stream |
| destructive | — | Destroy a consumer group for a Redis stream |
| write | — | Read entries from a Redis stream using a consumer group |
| write | — | Acknowledge entries that were processed by a consumer group |
| write | — | Set a Redis string value with an optional expiration time |
| read | — | Get a Redis string value |
json (3 tools)
Tool | Kind | Needs | Description |
| write |
| Set a JSON value in Redis at a given path with an optional expiration time |
| read |
| Retrieve a JSON value from Redis at a given path |
| destructive |
| Delete a JSON value from Redis at a given path |
search (6 tools)
Tool | Kind | Needs | Description |
| read |
| List of indexes in the Redis database |
| read |
| Retrieve schema and information about a specific Redis index using FT.INFO |
| read |
| Retrieve the number of indexed keys by the index |
| write |
| Create a Redis 8 vector similarity index using HNSW on a Redis hash |
| read |
| Perform a KNN vector similarity search using Redis 8 or later version on vectors stored in hash data structures |
| read |
| Perform a hybrid search combining a Redis filter expression with KNN vector similarity |
pubsub (5 tools)
Tool | Kind | Needs | Description |
| write | — | Publish a message to a Redis channel |
| read | — | Subscribe to a Redis channel and return a reusable subscription handle |
| read | — | Subscribe to Redis channels using a pattern |
| read | no | Read pending pub/sub messages for an existing subscription |
| read | no | Unsubscribe and close an existing pub/sub subscription |
docs (1 tool)
Tool | Kind | Needs | Description |
| read | no | 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, orTimeout reading from socketwhen 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.
MOVEDandASKredirects 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_timeoutandsocket_timeoutof 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 |
|
|
|
Connection refused |
|
|
|
Connection times out |
| 2 s per Sentinel: | 5 s per startup node: with one or two nodes |
Connection accepted, no answer |
| 2 s per Sentinel: | 10 s per startup node: with one node |
Server hangs after the client connected |
| the primary: | a node: |
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 endpointGET /healthzreturns{"status":"UP"}; it does not depend on Redis, so a cluster outage does not restart the pod. Useclusters_listto 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
apiKeymust 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 headerX-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 isX-Authorization, notAuthorization, and takes the raw key (noBearerprefix). An empty or missingapiKeywithout aREDIS_MCP_API_KEYturns the check off. The startup log saysAPI key authentication enabled for /mcp (header X-Authorization), or warnsAPI key authentication DISABLED (apiKey is empty): anyone who can reach /mcp can call the tools. On OpenShift, the key comes from the Secretredis-mcp-api-key(see OpenShift deployment).Browser requests are refused: a request to
/mcpwith anOriginheader (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 noOriginheader; browser-based clients cannot connect./healthzis 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).
readOnlycontrols which tools are listed; it does not replace ACLs.Keep secrets out of the repository.
redis-mcp.yamlin the repository root,*.local.yaml(such asdeploy/openshift/secret.local.yaml) andconf.d/are git-ignored, and the image build context contains onlysrc/,pyproject.toml,uv.lock,README.mdandLICENSE. Examples use<...>placeholders;deploy/openshift/configmap.yamlholds no secrets, anddeploy/openshift/secret.yamlonly 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}outcomeiserrorwhen the tool raises or hits the 15 s limit. Most data tools report Redis errors (for example a missing ACL permission orTimeout reading from socket) in their result text instead, which is logged asok. It iscancelled, witherror: "cancelled by the client", when the client cancels the request before it finishes.clusterisnullfor tools without a cluster, forclusters_listwithout one, and for calls that leave it out when only one cluster is configured.Calls rejected before they reach the tool (a
clusteroutside the enum, a missing or mistyped parameter, an unknown tool) produce notool callline.A rejected HTTP request logs
unauthorized requestatWARNINGwithremoteAddrandheaderPresent, 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.
/appand/etc/redis-mcpare group 0 withg=u, so a random UID in group 0 works.It writes nothing at runtime: the bytecode is compiled during the build and
uvis not used to start the server, so the root filesystem can be read-only, with anemptyDirmounted at/tmp. (uv runinside the container would fail as a non-root user:Failed to initialize cache at /.cache/uv.)CMD ["/app/.venv/bin/python", "src/main.py"]: withREDIS_MCP_CONFIG(orREDIS_MCP_CONFIG_DIR) set it runs the multi-cluster mode, otherwise the single-cluster mode from theREDIS_*variables over stdio. The OpenShift Deployment runs/app/.venv/bin/redis-mcp --config /etc/redis-mcp/config/redis-mcp.yamlinstead./app/.venv/binis onPATH, soredis-mcpalso works as a command. Port 8080 is exposed; keepport: 8080in the config to get HTTP.Mount points:
/etc/redis-mcp/config(redis-mcp.yaml),/etc/redis-mcp/passwords(<cluster>_passfiles),/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 |
|
tag |
|
manual run (Actions → Docker push → Run workflow) |
|
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 |
| The ConfigMap |
| 1 replica ( |
|
|
| Edge TLS (HTTP redirects to HTTPS), |
| The Secrets |
|
|
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.03. 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 laterredis-mcp-passwordsneeds one<cluster>_passkey per cluster (see Passwords, also for quoting the values). A cluster without a key connects without AUTH, with a warning in the log.api-keyis required: while the Secretredis-mcp-api-keydoes not exist, the pod does not start (CreateContainerConfigError), and whileapi-keyis 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 noapiKeyin the ConfigMap authentication is then off (API key authentication DISABLEDin 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-mcpApplied 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.crtThe 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-mcpA 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>
claudeIf 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.pemRun /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,
jsonandsearchincluded, and nothing is hidden byreadOnlyordisableDestructive;the tools'
clusterparameter is optional (see Which tools are listed);the transport is stdio, unless
--transport http(port 8080) or--http-portis given; there is no API key unless--api-key/REDIS_MCP_API_KEYsets 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 :8080Variable | Flag | Default | Meaning |
| not set |
| |
|
|
| Host name or IP address. |
|
|
| Port. |
|
|
| Database number (not used with cluster mode). |
|
| not set | ACL user; without it the password authenticates the |
|
| not set | Password. Prefer the variable: a command line can be read with |
|
| off | TLS ( |
|
| not set | CA bundle file ( |
|
| not set | CA directory ( |
|
| not set | Client certificate and key (mutual TLS). |
|
|
|
|
|
| off | Redis Cluster ( |
With the
redis-mcpcommand, 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, soREDIS_HOST,REDIS_PORT,REDIS_DB,REDIS_SSL,REDIS_SSL_CERT_REQSandREDIS_CLUSTER_MODEapply only topython src/main.py. The other variables apply to both, unless the flag is given.--cluster-modetogether with--ssland--ssl-ca-path(or the variables) stops withredis-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 |
|
|
|
|
|
|
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 |
|
|
| The key from |
| The key from |
Pod in | The Secrets do not exist in the pod's namespace: |
| Fix the named key, for example |
|
|
| The option's value breaks the rule in the "Value" column of Cluster options, for example |
| Put the CA certificates into one PEM bundle and set |
| The single-cluster mode's equivalent of the row above: use |
|
|
| Entry |
| Both clusters would read the password file |
| Set |
| 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. |
| The file is not valid YAML; fix it at the line and column named. The message does not quote the line. |
| Valid toolsets are |
| The connection flags belong to the single-cluster mode; define the clusters in the config file. |
HTTP 401 | The key does not match the server's key exactly. Check that |
HTTP 403 | The request came from a web page or a browser-based client. Use a non-browser MCP client such as Claude Code. |
Startup warning | The Secret has no |
| Wrong password or |
| The cluster's ACL user lacks the command. Expected for writes with |
| Either the cluster is not a Redis Cluster (use |
Results like | The cluster is a Redis Cluster but configured with |
| Check |
| The host name does not resolve from the pod. Check the name in |
| Without anything after it: the Sentinels answered, but none of them knows a healthy primary named |
|
|
| Run |
| Every primary of a Redis Cluster has its own cursor. Call |
| 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 |
| 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 | 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. |
| More calls ran on the cluster at once than its tools' client has connections, usually because |
A tool or cluster is missing from the list, or | Its toolset is not in |
| 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. |
| 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 templatesCommand | What it does |
| Creates |
| All tests, with coverage (fails under 80 %; reports in |
| One test file, without coverage |
| Lint |
| Format check ( |
| Security scan |
| Container image |
The suite has over 1,500 tests.
tests/test_connection.pyandtests/test_multi_cluster_integration.pystart their ownredis-serverprocesses on free ports (tests/redis_servers.py), and a Redis Cluster of three primaries built withredis-cli, when those binaries are installed; without them these tests are skipped.tests/test_integration.pystarts the server in the single-cluster mode against the Redis atREDIS_HOST/REDIS_PORT(defaultlocalhost: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 withredis-serverandredis-toolsinstalled, plus Windows and macOS without Redis), builds the wheel and checks thatredis-mcp --helpruns 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>"). PassToolKind.WRITEorToolKind.DESTRUCTIVEas the second argument for a tool that changes data,requires="json"orrequires="search"for a module tool, andcluster="optional"orcluster="none"for a tool that is not tied to one cluster. The function name becomes the tool name and the docstring its description. Theclusterparameter, theWRITE:prefix, the annotations, the time limit and thetool calllog line are added by the registry, and the modules ofsrc/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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted Upstash MCP server: Redis, QStash, Workflow, Vector, Search, Box and Blob, with OAuth.
Unified gateway exposing 150+ tools across all NexGenData MCP servers via one endpoint.
Manage remote servers, SSH terminals, files, databases and proxies with scoped access and approvals.
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceProvides access to Redis databases. This server enables LLMs to interact with Redis key-value stores through a set of standardized tools.148 npm30MIT
- AlicenseBqualityDmaintenanceEnables 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.342 npm2MIT
- FlicenseNot gradedqualityDmaintenanceRedisNexus 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.-
- AlicenseAqualityAmaintenanceEnables exploring and diagnosing a Redis instance from MCP clients with read-only safety, using SCAN instead of KEYS for safe key enumeration.7200 npm2MIT