redis-switchboard-mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +553 -2
- package/README.zh-CN.md +555 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +355 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +69 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +472 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch-guard.d.ts +13 -0
- package/dist/fetch-guard.d.ts.map +1 -0
- package/dist/fetch-guard.js +630 -0
- package/dist/fetch-guard.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +13 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +11 -0
- package/dist/lib.js.map +1 -0
- package/dist/redis-manager.d.ts +44 -0
- package/dist/redis-manager.d.ts.map +1 -0
- package/dist/redis-manager.js +455 -0
- package/dist/redis-manager.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +79 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/common.d.ts +5 -0
- package/dist/tools/common.d.ts.map +1 -0
- package/dist/tools/common.js +60 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/execute.d.ts +11 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +58 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/key-info.d.ts +11 -0
- package/dist/tools/key-info.d.ts.map +1 -0
- package/dist/tools/key-info.js +116 -0
- package/dist/tools/key-info.js.map +1 -0
- package/dist/tools/read-page.d.ts +26 -0
- package/dist/tools/read-page.d.ts.map +1 -0
- package/dist/tools/read-page.js +311 -0
- package/dist/tools/read-page.js.map +1 -0
- package/dist/tools/scan-keys.d.ts +22 -0
- package/dist/tools/scan-keys.d.ts.map +1 -0
- package/dist/tools/scan-keys.js +156 -0
- package/dist/tools/scan-keys.js.map +1 -0
- package/dist/truncate.d.ts +29 -0
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +200 -0
- package/dist/truncate.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +28 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hexiaowu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,554 @@
|
|
|
1
|
-
#
|
|
1
|
+
# redis-switchboard-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
A Redis MCP server that switches **logical databases per request** instead of relying on Redis's connection-level `SELECT`.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
execute → run a Redis command against db 2, db 4, ...
|
|
9
|
+
read_page → bounded page of a list / hash / set / zset / stream / string
|
|
10
|
+
scan_keys → cursor-based SCAN with limits
|
|
11
|
+
key_info → type / TTL / memory / encoding / length
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`redis-switchboard-mcp` is designed for the way MCP clients actually work: many independent tool calls, often concurrent, against several environments and several Redis logical databases.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why this exists
|
|
19
|
+
|
|
20
|
+
Redis's `SELECT` changes the database of a **connection**, not of a command. An MCP server that shares one connection and runs `SELECT` on demand will silently mix databases: a concurrent call, a pooled connection, or an automatic reconnect can leave the client on the wrong DB.
|
|
21
|
+
|
|
22
|
+
Tool calls never run `SELECT` themselves, and the server does not trust ioredis's connection-time `SELECT` (ioredis swallows a failed `SELECT` and still reports the connection as ready). Instead:
|
|
23
|
+
|
|
24
|
+
1. Every tool accepts a `db` argument.
|
|
25
|
+
2. Clients are cached per `(instance, db)` — one dedicated connection per logical database.
|
|
26
|
+
3. Each connection starts on db 0 and explicitly runs `SELECT db` for db > 0. The result is verified before any tool command runs; a failed `SELECT` disconnects and errors instead of silently using db 0.
|
|
27
|
+
4. After a reconnect, the target database is selected and verified again before commands are allowed.
|
|
28
|
+
5. `SELECT`, `MOVE`, `SWAPDB`, `COPY ... DB` and transaction/pub-sub state commands are rejected.
|
|
29
|
+
|
|
30
|
+
The result is stateless from the caller's point of view: `execute` in db 2 and db 4 can run concurrently without leaking state.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Per-call database selection** for every tool (`db: 0-255`).
|
|
37
|
+
- **Multiple Redis instances** from a JSON config, selected with the `instance` argument.
|
|
38
|
+
- **Explicit permission mode** (`readonly` / `readwrite`); `readonly` uses an explicit read-command whitelist.
|
|
39
|
+
- **Command blocklist** for connection-level, administrative, destructive and blocking commands.
|
|
40
|
+
- **Lua scripting disabled by default** (`EVAL`/`EVALSHA`/`FCALL`).
|
|
41
|
+
- **Bounded output** with per-container and global item/character budgets.
|
|
42
|
+
- **Paged collection reads** via `read_page`, so large lists/hashes/sets/streams are never fetched in one shot.
|
|
43
|
+
- **Fetch guards** in `execute`: unbounded `GET`/`MGET`/`HMGET`, `LRANGE 0 -1`, `HGETALL`/`HKEYS`/`HVALS`, `SMEMBERS`, `ZRANGE 0 -1`, `ZUNION`/`ZINTER`/`ZDIFF`, `XRANGE` without `COUNT`, huge `HSCAN`/`SSCAN`/`ZSCAN` `COUNT`, `SORT_RO` without `LIMIT`, negative random counts and oversized set operations are rejected with a pointer to `read_page`.
|
|
44
|
+
- **Bounded scans** with `limit`, SCAN iteration cap, and a wall-clock budget.
|
|
45
|
+
- **Cluster detection** — logical DBs other than 0 are rejected on a Redis Cluster.
|
|
46
|
+
- **Connection cache limit** per instance (`maxConnections`, default 32).
|
|
47
|
+
- **Credential redaction** in error messages.
|
|
48
|
+
- **No stdout logging** — stdout is reserved for the MCP stdio transport.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Node.js **20 or newer**
|
|
55
|
+
- Redis 6+ recommended (Redis 7+ for `SORT_RO` and some `SCAN TYPE` features)
|
|
56
|
+
- A standalone Redis server, or a Redis Cluster used at **db 0 only**
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
### From npm
|
|
63
|
+
|
|
64
|
+
> The npm package is not published yet. Use the source install below for now.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install -g redis-switchboard-mcp
|
|
68
|
+
rsm --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or run it without a global install:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx -y redis-switchboard-mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### From source
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/hexiaowu/redis-switchboard-mcp.git
|
|
81
|
+
cd redis-switchboard-mcp
|
|
82
|
+
npm ci
|
|
83
|
+
npm run build
|
|
84
|
+
node dist/index.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## MCP client configuration
|
|
90
|
+
|
|
91
|
+
### Single instance, environment variables
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"redis": {
|
|
97
|
+
"command": "npx",
|
|
98
|
+
"args": ["-y", "redis-switchboard-mcp"],
|
|
99
|
+
"env": {
|
|
100
|
+
"REDIS_HOST": "127.0.0.1",
|
|
101
|
+
"REDIS_PORT": "6379",
|
|
102
|
+
"REDIS_PASSWORD": "your-password",
|
|
103
|
+
"REDIS_MODE": "readonly"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If the package is installed globally, `"command": "rsm"` and no args also work.
|
|
111
|
+
|
|
112
|
+
### Multiple instances from a JSON file
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"redis": {
|
|
118
|
+
"command": "npx",
|
|
119
|
+
"args": [
|
|
120
|
+
"-y",
|
|
121
|
+
"redis-switchboard-mcp",
|
|
122
|
+
"--config",
|
|
123
|
+
"/Users/me/.config/redis-switchboard.config.json"
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The same file can be selected with `REDIS_MCP_CONFIG=/path/to/config.json`.
|
|
131
|
+
|
|
132
|
+
> **Note:** diagnostics are written to stderr. Never print to stdout from this process — the MCP transport owns it.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
<h2 align="center">⚙️ Configuration</h2>
|
|
137
|
+
|
|
138
|
+
<h3 align="center">🔀 Precedence</h3>
|
|
139
|
+
|
|
140
|
+
1. Command-line flags (`--config`, `--db`, ...)
|
|
141
|
+
2. Environment variables
|
|
142
|
+
3. Defaults
|
|
143
|
+
|
|
144
|
+
`--config` selects a JSON config file. `--name`/`--db` apply only to the single-instance environment form; edit the JSON file for per-instance values.
|
|
145
|
+
|
|
146
|
+
<h3 align="center">🎛️ Command-line options</h3>
|
|
147
|
+
|
|
148
|
+
<p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
|
|
149
|
+
|
|
150
|
+
| Option | Required | Description |
|
|
151
|
+
| :---: | :---: | :---: |
|
|
152
|
+
| `--config <path>` | 🔶 Conditional | JSON config file describing one or more Redis instances |
|
|
153
|
+
| `--name <name>` | ⚪ Optional | Instance name (default: `default`) |
|
|
154
|
+
| `--db <n>` | ⚪ Optional | Default logical database, 0-255 (default: 0) |
|
|
155
|
+
| `--mode <mode>` | ⚪ Optional | Override permission mode for all instances: `readonly` or `readwrite` |
|
|
156
|
+
| `-h`, `--help` | ⚪ Optional | Show help |
|
|
157
|
+
| `-v`, `--version` | ⚪ Optional | Show the version |
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
<h3 align="center">🌱 Environment variables (single instance)</h3>
|
|
161
|
+
|
|
162
|
+
<p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
|
|
163
|
+
|
|
164
|
+
| Variable | Required | Default | Description |
|
|
165
|
+
| :---: | :---: | :---: | :---: |
|
|
166
|
+
| `REDIS_MCP_CONFIG` | 🔶 Conditional | — | Path to a multi-instance JSON config file |
|
|
167
|
+
| `REDIS_HOST` | ✅ Required | — | Redis host |
|
|
168
|
+
| `REDIS_PORT` | ✅ Required | — | Redis port |
|
|
169
|
+
| `REDIS_USERNAME` | ⚪ Optional | — | ACL username |
|
|
170
|
+
| `REDIS_PASSWORD` | 🔶 Conditional | — | Password (required when Redis requires authentication) |
|
|
171
|
+
| `REDIS_DB` | ⚪ Optional | `0` | Default logical database when `db` is omitted |
|
|
172
|
+
| `REDIS_TLS` | ⚪ Optional | `false` | Enable TLS |
|
|
173
|
+
| `REDIS_TLS_REJECT_UNAUTHORIZED` | ⚪ Optional | `true` | Verify the server certificate |
|
|
174
|
+
| `REDIS_MODE` | ✅ Required | — | Permission mode: `readonly` or `readwrite` (required for env config unless `--mode` is used) |
|
|
175
|
+
| `REDIS_ALLOWED_DBS` | ⚪ Optional | — | Comma-separated allowlist, e.g. `0,2,4` |
|
|
176
|
+
| `REDIS_ALLOW_SCRIPTING` | ⚪ Optional | `false` | Allow `EVAL`/`EVALSHA`/`FCALL` (see security caveat) |
|
|
177
|
+
| `REDIS_MAX_CONNECTIONS` | ⚪ Optional | `32` | Per-instance connection cache limit (1-256) |
|
|
178
|
+
| `REDIS_CONNECT_TIMEOUT_MS` | ⚪ Optional | `5000` | TCP/connect timeout |
|
|
179
|
+
| `REDIS_COMMAND_TIMEOUT_MS` | ⚪ Optional | `15000` | Per-command timeout; `0` disables it |
|
|
180
|
+
| `REDIS_MAX_RESULT_ITEMS` | ⚪ Optional | `200` | Per-array/object item cap in replies |
|
|
181
|
+
| `REDIS_MAX_RESULT_CHARS` | ⚪ Optional | `4000` | Per-string cap in replies |
|
|
182
|
+
| `REDIS_MAX_RESULT_DEPTH` | ⚪ Optional | `4` | Maximum nested container depth |
|
|
183
|
+
| `REDIS_MAX_FETCH_ITEMS` | ⚪ Optional | `1000` | Max items one `execute` bulk read may fetch |
|
|
184
|
+
| `REDIS_MAX_FETCH_CHARS` | ⚪ Optional | `1000000` | Max bytes one `execute` string read may fetch |
|
|
185
|
+
| `REDIS_SCAN_COUNT` | ⚪ Optional | `500` | Default SCAN `COUNT` hint |
|
|
186
|
+
| `REDIS_MAX_SCAN_ITERATIONS` | ⚪ Optional | `200` | Maximum SCAN round trips per call |
|
|
187
|
+
| `REDIS_SCAN_TIME_BUDGET_MS` | ⚪ Optional | `10000` | Wall-clock budget per `scan_keys` call |
|
|
188
|
+
|
|
189
|
+
<p align="center"><strong>Connection methods:</strong> choose one of the following.</p>
|
|
190
|
+
|
|
191
|
+
| Method | Required | How |
|
|
192
|
+
| :---: | :---: | :---: |
|
|
193
|
+
| JSON config | 🔶 Conditional | Set `REDIS_MCP_CONFIG` (or `--config`) |
|
|
194
|
+
| Host + port | ✅ Required | Set `REDIS_HOST` and `REDIS_PORT` |
|
|
195
|
+
|
|
196
|
+
For the single-instance environment form, `REDIS_HOST` and `REDIS_PORT` are required — there is no built-in host or port default. The JSON config alternative defines `host`/`port` for each instance.
|
|
197
|
+
|
|
198
|
+
JSON config and `REDIS_HOST`/`REDIS_PORT`/`REDIS_MODE` are **mutually exclusive**: setting them with a JSON config is a configuration error (the process refuses to start and tells you to define host/port/mode per instance in the JSON file). Other single-instance environment variables are not applied when a JSON config is used.
|
|
199
|
+
|
|
200
|
+
<h3 align="center">🗂️ Multi-instance JSON</h3>
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"defaultInstance": "cache-dev",
|
|
205
|
+
"instances": [
|
|
206
|
+
{
|
|
207
|
+
"name": "cache-dev",
|
|
208
|
+
"host": "127.0.0.1",
|
|
209
|
+
"port": 6379,
|
|
210
|
+
"db": 0,
|
|
211
|
+
"mode": "readwrite",
|
|
212
|
+
"allowedDbs": [0, 2, 4],
|
|
213
|
+
"maxConnections": 32
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "cache-prod",
|
|
217
|
+
"host": "redis.prod.internal",
|
|
218
|
+
"port": 6380,
|
|
219
|
+
"username": "app",
|
|
220
|
+
"passwordEnv": "PROD_REDIS_PASSWORD",
|
|
221
|
+
"tls": true,
|
|
222
|
+
"db": 0,
|
|
223
|
+
"mode": "readonly",
|
|
224
|
+
"allowedDbs": [0],
|
|
225
|
+
"commandTimeoutMs": 10000,
|
|
226
|
+
"maxResultItems": 100,
|
|
227
|
+
"maxResultChars": 2000,
|
|
228
|
+
"maxFetchItems": 500,
|
|
229
|
+
"maxFetchChars": 500000
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
<p align="center"><strong>Top-level fields</strong></p>
|
|
236
|
+
|
|
237
|
+
<p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
|
|
238
|
+
|
|
239
|
+
| Field | Required | Default | Description |
|
|
240
|
+
| :---: | :---: | :---: | :---: |
|
|
241
|
+
| `instances` | ✅ Required | — | Array with at least one instance |
|
|
242
|
+
| `defaultInstance` | ⚪ Optional | — | Instance used when a tool call omits `instance` |
|
|
243
|
+
| `$schema` | ⚪ Optional | — | Ignored; useful for editor schema hints |
|
|
244
|
+
|
|
245
|
+
<p align="center"><strong>Instance fields</strong></p>
|
|
246
|
+
|
|
247
|
+
<p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
|
|
248
|
+
|
|
249
|
+
| Field | Required | Default | Description |
|
|
250
|
+
| :---: | :---: | :---: | :---: |
|
|
251
|
+
| `name` | 🔶 Conditional | `default` | Unique instance name; must be unique when several instances are configured |
|
|
252
|
+
| `host` / `port` | ✅ Required | — | Connection endpoint |
|
|
253
|
+
| `username` | ⚪ Optional | — | ACL username |
|
|
254
|
+
| `password` | 🔶 Conditional | — | Inline password (required when Redis requires authentication) |
|
|
255
|
+
| `usernameEnv` / `passwordEnv` | ⚪ Optional | — | Read credentials from an environment variable |
|
|
256
|
+
| `db` | ⚪ Optional | `0` | Default logical database |
|
|
257
|
+
| `tls` | ⚪ Optional | `false` | Enable TLS |
|
|
258
|
+
| `tlsRejectUnauthorized` | ⚪ Optional | `true` | Verify the server certificate |
|
|
259
|
+
| `mode` | ✅ Required | — | Permission mode: `readonly` or `readwrite` |
|
|
260
|
+
| `allowScripting` | ⚪ Optional | `false` | Allow `EVAL`/`EVALSHA`/`FCALL` |
|
|
261
|
+
| `allowedDbs` | ⚪ Optional | — | Restrict which logical DBs this instance may access |
|
|
262
|
+
| `maxConnections` | ⚪ Optional | `32` | Cache limit for `(instance, db)` connections |
|
|
263
|
+
| `connectTimeoutMs` | ⚪ Optional | `5000` | TCP/connect timeout |
|
|
264
|
+
| `commandTimeoutMs` | ⚪ Optional | `15000` | Per-command timeout; `0` disables |
|
|
265
|
+
| `maxResultItems` | ⚪ Optional | `200` | Per-container item cap |
|
|
266
|
+
| `maxResultChars` | ⚪ Optional | `4000` | Per-string character cap |
|
|
267
|
+
| `maxResultDepth` | ⚪ Optional | `4` | Nested container depth limit |
|
|
268
|
+
| `maxFetchItems` | ⚪ Optional | `1000` | Max items one `execute` bulk read may fetch |
|
|
269
|
+
| `maxFetchChars` | ⚪ Optional | `1000000` | Max bytes one `execute` string read may fetch |
|
|
270
|
+
| `scanCount` | ⚪ Optional | `500` | Default SCAN `COUNT` hint |
|
|
271
|
+
| `maxScanIterations` | ⚪ Optional | `200` | SCAN round-trip cap |
|
|
272
|
+
| `scanTimeBudgetMs` | ⚪ Optional | `10000` | `scan_keys` wall-clock budget |
|
|
273
|
+
|
|
274
|
+
Use `passwordEnv` rather than storing a password in the file, and keep the file `chmod 600`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Tools
|
|
279
|
+
|
|
280
|
+
### `execute`
|
|
281
|
+
|
|
282
|
+
Run one Redis command against a specific logical database.
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{ "command": "GET user:42", "db": 2 }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{ "command": ["HGET", "user:42", "name"], "db": 4, "instance": "cache-prod" }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- `command` is a command line (quotes and backslash escapes supported) or an argument array.
|
|
293
|
+
- `db` selects the logical database per call. It defaults to the instance's `db`.
|
|
294
|
+
- `instance` is required when several instances are configured without a default.
|
|
295
|
+
- Write commands are rejected when the instance `mode` is `readonly`.
|
|
296
|
+
- `SELECT` is always rejected — pass `db` instead.
|
|
297
|
+
- Bulk reads are guarded before execution: `GET`/`MGET`/`GETDEL`/`GETEX`/`GETSET`, `GETRANGE`, `LRANGE`, `HGETALL`/`HKEYS`/`HVALS`, `SMEMBERS`, `ZRANGE`/`ZRANGEBY*`/`ZUNION`/`ZINTER`/`ZDIFF`, `XRANGE`, `SORT`/`SORT_RO`, `LCS`, pop counts, negative random counts and oversized set operations are checked against `maxFetchItems`/`maxFetchChars` and rejected with `COMMAND_REJECTED` when unbounded. `HMGET` is limited by requested field count. Use `read_page` or a bounded variant. The guard is advisory (metadata is checked just before the command), not atomic.
|
|
298
|
+
|
|
299
|
+
Response:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"instance": "cache-dev",
|
|
304
|
+
"db": 2,
|
|
305
|
+
"command": "GET",
|
|
306
|
+
"argCount": 1,
|
|
307
|
+
"elapsedMs": 1,
|
|
308
|
+
"reply": "alice",
|
|
309
|
+
"truncated": false
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
When a reply is cut, `truncated` is `true` and `notes` explains which parts were shortened.
|
|
314
|
+
|
|
315
|
+
### `read_page`
|
|
316
|
+
|
|
317
|
+
Read **one bounded page** from a large collection. This is the safe alternative to `HGETALL`, `SMEMBERS`, `LRANGE key 0 -1`, `GET` on a big string, etc.
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{ "kind": "list", "key": "queue", "db": 2, "offset": 0, "limit": 100 }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| `kind` | Redis command | Paging |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `list` | `LRANGE key offset offset+limit-1` | `nextOffset` |
|
|
326
|
+
| `string` | `GETRANGE key offset offset+limit-1` (`limit` = bytes) | `nextOffset` |
|
|
327
|
+
| `hash` | `HSCAN key cursor COUNT limit [MATCH pattern]` | `nextCursor` |
|
|
328
|
+
| `set` | `SSCAN key cursor COUNT limit [MATCH pattern]` | `nextCursor` |
|
|
329
|
+
| `zset` | `ZRANGE key offset offset+limit-1 [WITHSCORES]` | `nextOffset` |
|
|
330
|
+
| `stream` | `XRANGE key start end COUNT limit` | `nextStart` |
|
|
331
|
+
|
|
332
|
+
Response (list):
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"instance": "cache-dev",
|
|
337
|
+
"db": 2,
|
|
338
|
+
"kind": "list",
|
|
339
|
+
"key": "queue",
|
|
340
|
+
"limit": 100,
|
|
341
|
+
"items": ["job:1", "job:2"],
|
|
342
|
+
"offset": 0,
|
|
343
|
+
"returned": 2,
|
|
344
|
+
"total": 12345,
|
|
345
|
+
"nextOffset": 2,
|
|
346
|
+
"complete": false,
|
|
347
|
+
"outputBytes": 12
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- `limit` defaults to 100 and is capped at 1,000, by `maxFetchItems`, and by an output budget derived from `maxFetchChars` and `maxResultChars`. `outputBytes` reports the page size in bytes.
|
|
352
|
+
- `limit` bounds the number of elements/fields, not the byte size of one element. A single large list element or hash value is still fetched in full; use `kind: "string"` with `GETRANGE` for large string values.
|
|
353
|
+
- Per-item strings are truncated at `maxResultChars`; `truncatedItems` reports how many were cut. `notes` reports limit reductions and SCAN batch overflows.
|
|
354
|
+
- `hash`/`set` use SCAN, so one call may return slightly more than `COUNT`; the whole batch is returned to avoid dropping fields/members, and `outputBytes` reports the actual size (it can exceed the page budget by one SCAN batch).
|
|
355
|
+
- For `kind: "string"`, `offset` and `limit` are **byte** offsets (Redis `GETRANGE`/`STRLEN` are byte-based). Pages are kept on UTF-8 character boundaries: a trailing incomplete character is left for the next page, so a page can return a few bytes more than `limit`.
|
|
356
|
+
- For stream paging, pass `nextStart` back as `start`; it is exclusive (`(` prefix). A page that exactly fills `limit` may need one extra empty request to confirm `complete`.
|
|
357
|
+
- For hash/set paging, pass `nextCursor` back as `cursor`.
|
|
358
|
+
|
|
359
|
+
### `scan_keys`
|
|
360
|
+
|
|
361
|
+
Cursor-based `SCAN` (never `KEYS`).
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{ "pattern": "session:*", "db": 2, "limit": 200, "type": "hash" }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Response:
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"instance": "cache-dev",
|
|
372
|
+
"db": 2,
|
|
373
|
+
"pattern": "session:*",
|
|
374
|
+
"type": "hash",
|
|
375
|
+
"cursor": "18432",
|
|
376
|
+
"keys": ["session:1", "session:2"],
|
|
377
|
+
"returned": 2,
|
|
378
|
+
"limit": 200,
|
|
379
|
+
"complete": false,
|
|
380
|
+
"hasMore": true,
|
|
381
|
+
"scannedEntries": 412,
|
|
382
|
+
"iterations": 2,
|
|
383
|
+
"elapsedMs": 3,
|
|
384
|
+
"outputChars": 42,
|
|
385
|
+
"duplicateKeys": 0,
|
|
386
|
+
"truncatedKeyNames": 0,
|
|
387
|
+
"notes": ["Result limit (200) reached; returned 240 key(s), rounded up to the SCAN batch boundary so no keys were dropped. Pass the returned cursor to continue."]
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Pass `cursor` back unchanged (prefer the string form) to continue. Notes:
|
|
392
|
+
|
|
393
|
+
- SCAN is not a snapshot; keys may appear, disappear or be returned twice across calls.
|
|
394
|
+
- Duplicates **inside one call** are removed.
|
|
395
|
+
- The call stops after `limit` (rounded up to the current SCAN batch), when the cursor reaches `0`, after `maxScanIterations`, when the ~1 MB key-name budget is reached, or when the time budget is exhausted. Keys from the current batch are always returned, so a call can exceed `limit` and the key-name budget by up to one batch (`count` is clamped to 1,000, so the worst case is roughly 4 MB with 4,096-character keys).
|
|
396
|
+
- `count` is clamped to 1,000 to keep one batch bounded; key names are capped at 4,096 characters.
|
|
397
|
+
- `scannedEntries` is the number of names returned by SCAN batches, not an exact count of visited keys.
|
|
398
|
+
|
|
399
|
+
### `key_info`
|
|
400
|
+
|
|
401
|
+
Metadata only — never the value itself.
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
{ "key": "user:42", "db": 2 }
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Response:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"instance": "cache-dev",
|
|
412
|
+
"db": 2,
|
|
413
|
+
"key": "user:42",
|
|
414
|
+
"exists": true,
|
|
415
|
+
"type": "hash",
|
|
416
|
+
"ttlMs": 3600000,
|
|
417
|
+
"persistent": false,
|
|
418
|
+
"memoryUsageBytes": 184,
|
|
419
|
+
"encoding": "listpack",
|
|
420
|
+
"length": 3,
|
|
421
|
+
"lengthUnit": "items"
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`lengthUnit` is `bytes` for strings, `entries` for streams, and `items` for lists/hashes/sets/sorted sets. If `MEMORY USAGE` or `OBJECT ENCODING` is unavailable (old Redis, restrictive ACL), the field is `null` and a `warnings` array is added.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Security model
|
|
430
|
+
|
|
431
|
+
### Permission modes
|
|
432
|
+
|
|
433
|
+
The permission mode is an explicit enum. There is no `readOnly` boolean and no implicit default:
|
|
434
|
+
|
|
435
|
+
| Mode | Behavior |
|
|
436
|
+
| :---: | :---: |
|
|
437
|
+
| `readonly` | Only an explicit read-command whitelist is accepted; `SET`, `HSET`, `DEL`, `RESTORE`, ... are rejected. `EVAL`/`FCALL` remain rejected even if `allowScripting` is true. |
|
|
438
|
+
| `readwrite` | Write commands such as `SET`, `HSET`, `DEL` are allowed; the always-rejected list below still applies, and `EVAL`/`FCALL` are allowed only when `allowScripting` is true. |
|
|
439
|
+
|
|
440
|
+
Set it in JSON as `"mode": "readonly"` / `"mode": "readwrite"`, in environment configuration with `REDIS_MODE=readonly|readwrite` (required unless `--mode` is provided), or override every instance with `--mode readonly|readwrite`.
|
|
441
|
+
|
|
442
|
+
`REDIS_MODE` cannot be combined with a JSON config file; set `mode` per instance instead (or use `--mode` to override all instances).
|
|
443
|
+
|
|
444
|
+
Use a Redis ACL with a read-only user as a second layer of defense — `readonly` mode is a convenience, not a replacement for server-side ACLs.
|
|
445
|
+
|
|
446
|
+
**Migration:** `readOnly` (JSON), `REDIS_READONLY` and `--readonly` were removed. Use `mode`, `REDIS_MODE` and `--mode` instead.
|
|
447
|
+
|
|
448
|
+
### Always rejected
|
|
449
|
+
|
|
450
|
+
| Category | Examples |
|
|
451
|
+
| --- | --- |
|
|
452
|
+
| Database / connection state | `SELECT`, `MOVE`, `SWAPDB`, `COPY ... DB`, `MULTI`, `EXEC`, `WATCH`, `RESET`, `HELLO`, `AUTH`, `QUIT` |
|
|
453
|
+
| Pub/sub and streaming | `SUBSCRIBE`, `PSUBSCRIBE`, `MONITOR`, `SYNC`, `PSYNC` |
|
|
454
|
+
| Administration | `CONFIG`, `DEBUG`, `CLIENT`, `ACL`, `MODULE`, `SHUTDOWN`, `SCRIPT`, `FUNCTION` |
|
|
455
|
+
| Topology / replication | `CLUSTER`, `REPLICAOF`, `SLAVEOF`, `FAILOVER`, `MIGRATE`, `REPLCONF` |
|
|
456
|
+
| Destructive / server-wide | `FLUSHDB`, `FLUSHALL`, `SAVE`, `BGSAVE`, `BGREWRITEAOF` |
|
|
457
|
+
| Blocking | `BLPOP`, `BRPOP`, `BLMOVE`, `BZPOPMIN`, `WAIT`, `XREAD`, `XREADGROUP`, ... |
|
|
458
|
+
| Slow key enumeration | `KEYS`, raw `SCAN` — use `scan_keys` |
|
|
459
|
+
|
|
460
|
+
Blocking commands are rejected because they hold a shared per-database connection and can stall every later request on that DB.
|
|
461
|
+
|
|
462
|
+
### Lua scripting
|
|
463
|
+
|
|
464
|
+
`EVAL`, `EVALSHA`, `EVAL_RO`, `EVALSHA_RO`, `FCALL` and `FCALL_RO` are **disabled by default**. `allowScripting: true` / `REDIS_ALLOW_SCRIPTING=1` enables them for trusted callers.
|
|
465
|
+
|
|
466
|
+
> A Lua script can call commands that the blocklist otherwise rejects, and depending on the Redis version it may be able to reach other logical databases. Treat `allowScripting` as disabling both the blocklist and the `allowedDbs` boundary: only enable it for trusted callers and enforce limits with a Redis ACL.
|
|
467
|
+
|
|
468
|
+
### Output and fetch bounds
|
|
469
|
+
|
|
470
|
+
- Per array/object: `maxResultItems`
|
|
471
|
+
- Per string/buffer: `maxResultChars`
|
|
472
|
+
- Nesting: `maxResultDepth`
|
|
473
|
+
- Whole reply: global item and character budgets derived from the above (roughly 1000+ items / 100 KB-1 MB), preventing nested replies from multiplying into gigabytes
|
|
474
|
+
- `execute` bulk reads: `maxFetchItems` (default 1000) and `maxFetchChars` (default 1,000,000) are checked **before** the command runs; unbounded `GET`, `LRANGE`, `HGETALL`, `SMEMBERS`, `ZRANGE`, `XRANGE` and oversized set operations are rejected
|
|
475
|
+
- `MGET` is limited to 100 keys per call; `HMGET` is limited to `maxFetchItems` requested fields
|
|
476
|
+
- `read_page`: server-side paging with `limit` capped at 1,000 / `maxFetchItems`
|
|
477
|
+
- `scan_keys`: `limit`, `maxScanIterations`, `scanTimeBudgetMs`
|
|
478
|
+
- `maxConnections`: bounds total sockets per instance
|
|
479
|
+
|
|
480
|
+
### Credentials
|
|
481
|
+
|
|
482
|
+
- A Redis user must be allowed to run `SELECT` to use any database other than 0. Without `+select`, `db > 0` fails closed with a `DB_ERROR` instead of silently using db 0.
|
|
483
|
+
- Prefer `passwordEnv` / `REDIS_PASSWORD` over inline JSON passwords.
|
|
484
|
+
- Keep a config file containing secrets at `chmod 600`.
|
|
485
|
+
- Error messages redact `password=...`, quoted/JSON password fields, echoed `AUTH` arguments, and any connection URL that may appear in an error.
|
|
486
|
+
- TLS certificate verification is on by default; `tlsRejectUnauthorized: false` disables it and should only be used for self-signed development servers.
|
|
487
|
+
|
|
488
|
+
### Transport
|
|
489
|
+
|
|
490
|
+
This package exposes a **stdio** MCP server. It does not open an HTTP port and does not implement MCP authentication. The MCP client that launches it is the trust boundary.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Behavior and limitations
|
|
495
|
+
|
|
496
|
+
- **No `SELECT`**: logical DB selection is per call. This is the point of the project.
|
|
497
|
+
- **Redis Cluster**: only db 0 exists. The server detects `cluster_enabled:1` and rejects db > 0. It connects to a single node, so keys owned by other slots can return `MOVED`; it is not a cluster-aware sharding client.
|
|
498
|
+
- **No transactions**: `MULTI`/`EXEC`/`WATCH` are connection-level and would leak between requests.
|
|
499
|
+
- **No pub/sub or `MONITOR`**: they change the connection mode.
|
|
500
|
+
- **No blocking commands**: they would pin a shared connection. Use non-blocking variants (`XRANGE` instead of blocking `XREAD`, etc.).
|
|
501
|
+
- **Command timeouts are client-side**: ioredis rejects the promise but does not cancel the command in Redis, so a timed-out write may still have been applied. Verify state before retrying. `commandTimeoutMs: 0` disables the timeout; a very slow normal command can then occupy its connection.
|
|
502
|
+
- **Commands in flight during a disconnect are not resent**: `autoResendUnfulfilledCommands` is disabled to avoid applying a non-idempotent write twice. A command that was sent when the socket dropped can fail even though Redis may have executed it.
|
|
503
|
+
- **Binary values**: replies are decoded as UTF-8 strings. `DUMP` and other binary payloads may not round-trip; buffers are returned as bounded base64 when the client produces them.
|
|
504
|
+
- **Metadata commands for guards**: `execute`'s fetch guard issues `STRLEN`, `LLEN`, `HLEN`, `SCARD`, `ZCARD`, `XLEN`, `TYPE` and `MEMORY USAGE`; `read_page` issues the corresponding size command for its `kind`. The Redis user must be allowed to run them; `NOPERM` on a size/type command fails closed.
|
|
505
|
+
- **Guard overhead and false positives**: a guarded `execute` adds 1-3 metadata round trips (up to 100 for `MGET`, up to 64 for set/zset operations), and `MEMORY USAGE` is a slow command. The byte check uses whole-key `MEMORY USAGE`, so a very large key can be rejected even for a small `LRANGE`/`ZRANGE`/`XRANGE` page; use `read_page` for page-sized reads. The guard is advisory (TOCTOU): a concurrent writer can grow a key after the check.
|
|
506
|
+
- **Fetch guard coverage**: covered forms include `GET`/`MGET`/`GETDEL`/`GETEX`/`GETSET`, `GETRANGE`/`SUBSTR`, `HGET`, `DUMP`, `LRANGE`, `HGETALL`/`HKEYS`/`HVALS`, `SMEMBERS`, `SINTER`/`SUNION`/`SDIFF`, `ZRANGE`/`ZRANGEBY*`/`ZUNION`/`ZINTER`/`ZDIFF`, `XRANGE`/`XREVRANGE`, `HSCAN`/`SSCAN`/`ZSCAN` `COUNT`, `SORT`/`SORT_RO`, `LCS`, `GEOSEARCH`/`GEORADIUS_RO`/`GEORADIUSBYMEMBER_RO`, random members and pop counts. It is **not exhaustive**: `LINDEX`, `XPENDING`, `XINFO STREAM ... FULL`, `SET ... GET`, `LMPOP`/`ZMPOP` are not guarded, and item-count-only checks do not bound the byte size of an individual field/member. For those, use a restricted ACL, `commandTimeoutMs`, and the smallest possible command.
|
|
507
|
+
- **Large reads**: `execute` rejects the common unbounded bulk-read forms and `read_page` provides server-side paging. A command that is not covered by the guard (for example an unusual command with a large reply) can still be fully materialized by Redis/ioredis before `truncateReply` runs. Prefer `key_info` for metadata, `scan_keys` for key enumeration, `read_page` for collections, and a restricted ACL / `commandTimeoutMs` for very large data.
|
|
508
|
+
- **SCAN consistency**: SCAN is incremental and not a snapshot; results can be missed or duplicated across calls.
|
|
509
|
+
- **Connection cache**: each `(instance, db)` pair uses one connection. The default cap is 32 per instance. The cap is a hard stop with no eviction: once 32 distinct databases have been opened, other databases are unavailable until the process restarts. Raise `maxConnections` if you legitimately use many logical DBs.
|
|
510
|
+
- **Cluster + ACL**: if `INFO cluster` is denied by ACL, the explicit `SELECT` verification still rejects db > 0 on a cluster with a clear error. db 0 does not require `SELECT`.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Development
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
npm ci
|
|
518
|
+
npm run check # typecheck src + tests, then run the test suite
|
|
519
|
+
npm run build # compile to dist/
|
|
520
|
+
npm run dev # run from src with tsx
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The test suite uses a small in-process RESP server (`test/fake-redis-server.ts`), so it does **not** require a local Redis. It covers:
|
|
524
|
+
|
|
525
|
+
- command tokenization, blocklist and readonly policy
|
|
526
|
+
- reply truncation and global budgets
|
|
527
|
+
- environment / JSON / CLI configuration
|
|
528
|
+
- per-database client isolation and promise caching
|
|
529
|
+
- connection cache limits and error mapping
|
|
530
|
+
- `execute`, `read_page`, `scan_keys`, `key_info` end-to-end over real ioredis
|
|
531
|
+
- fetch guards for `GET` / `LRANGE` / `HGETALL` / `SMEMBERS` / `ZRANGE` / `XRANGE` / random members / set operations
|
|
532
|
+
- MCP handshake and `tools/list` over stdio
|
|
533
|
+
|
|
534
|
+
Project layout:
|
|
535
|
+
|
|
536
|
+
```text
|
|
537
|
+
src/
|
|
538
|
+
index.ts CLI entry and stdio transport
|
|
539
|
+
lib.ts Side-effect-free library entry
|
|
540
|
+
server.ts MCP server + tool registration
|
|
541
|
+
config.ts env / JSON / CLI configuration
|
|
542
|
+
commands.ts tokenizer, blocklist, readonly whitelist
|
|
543
|
+
truncate.ts bounded reply serialization
|
|
544
|
+
redis-manager.ts per-(instance, db) client cache
|
|
545
|
+
fetch-guard.ts preflight size guards for execute bulk reads
|
|
546
|
+
tools/ execute, read_page, scan_keys, key_info handlers
|
|
547
|
+
test/ unit, integration and MCP handshake tests
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## License
|
|
553
|
+
|
|
554
|
+
MIT
|