redis-switchboard-mcp 0.0.0-stage → 0.1.1

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +551 -2
  3. package/README.zh-CN.md +553 -0
  4. package/dist/commands.d.ts +15 -0
  5. package/dist/commands.d.ts.map +1 -0
  6. package/dist/commands.js +355 -0
  7. package/dist/commands.js.map +1 -0
  8. package/dist/config.d.ts +69 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +472 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/errors.d.ts +25 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +49 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/fetch-guard.d.ts +13 -0
  17. package/dist/fetch-guard.d.ts.map +1 -0
  18. package/dist/fetch-guard.js +630 -0
  19. package/dist/fetch-guard.js.map +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +104 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/lib.d.ts +13 -0
  25. package/dist/lib.d.ts.map +1 -0
  26. package/dist/lib.js +11 -0
  27. package/dist/lib.js.map +1 -0
  28. package/dist/redis-manager.d.ts +44 -0
  29. package/dist/redis-manager.d.ts.map +1 -0
  30. package/dist/redis-manager.js +455 -0
  31. package/dist/redis-manager.js.map +1 -0
  32. package/dist/server.d.ts +5 -0
  33. package/dist/server.d.ts.map +1 -0
  34. package/dist/server.js +79 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/tools/common.d.ts +5 -0
  37. package/dist/tools/common.d.ts.map +1 -0
  38. package/dist/tools/common.js +60 -0
  39. package/dist/tools/common.js.map +1 -0
  40. package/dist/tools/execute.d.ts +11 -0
  41. package/dist/tools/execute.d.ts.map +1 -0
  42. package/dist/tools/execute.js +58 -0
  43. package/dist/tools/execute.js.map +1 -0
  44. package/dist/tools/key-info.d.ts +11 -0
  45. package/dist/tools/key-info.d.ts.map +1 -0
  46. package/dist/tools/key-info.js +116 -0
  47. package/dist/tools/key-info.js.map +1 -0
  48. package/dist/tools/read-page.d.ts +26 -0
  49. package/dist/tools/read-page.d.ts.map +1 -0
  50. package/dist/tools/read-page.js +311 -0
  51. package/dist/tools/read-page.js.map +1 -0
  52. package/dist/tools/scan-keys.d.ts +22 -0
  53. package/dist/tools/scan-keys.d.ts.map +1 -0
  54. package/dist/tools/scan-keys.js +156 -0
  55. package/dist/tools/scan-keys.js.map +1 -0
  56. package/dist/truncate.d.ts +29 -0
  57. package/dist/truncate.d.ts.map +1 -0
  58. package/dist/truncate.js +200 -0
  59. package/dist/truncate.js.map +1 -0
  60. package/dist/version.d.ts +6 -0
  61. package/dist/version.d.ts.map +1 -0
  62. package/dist/version.js +28 -0
  63. package/dist/version.js.map +1 -0
  64. 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,552 @@
1
- # Temporary Holding Version
1
+ # redis-switchboard-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ```bash
65
+ npm install -g redis-switchboard-mcp
66
+ rsm --help
67
+ ```
68
+
69
+ Or run it without a global install:
70
+
71
+ ```bash
72
+ npx -y redis-switchboard-mcp
73
+ ```
74
+
75
+ ### From source
76
+
77
+ ```bash
78
+ git clone https://github.com/hexiaowu/redis-switchboard-mcp.git
79
+ cd redis-switchboard-mcp
80
+ npm ci
81
+ npm run build
82
+ node dist/index.js
83
+ ```
84
+
85
+ ---
86
+
87
+ ## MCP client configuration
88
+
89
+ ### Single instance, environment variables
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "redis": {
95
+ "command": "npx",
96
+ "args": ["-y", "redis-switchboard-mcp"],
97
+ "env": {
98
+ "REDIS_HOST": "127.0.0.1",
99
+ "REDIS_PORT": "6379",
100
+ "REDIS_PASSWORD": "your-password",
101
+ "REDIS_MODE": "readonly"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ If the package is installed globally, `"command": "rsm"` and no args also work.
109
+
110
+ ### Multiple instances from a JSON file
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "redis": {
116
+ "command": "npx",
117
+ "args": [
118
+ "-y",
119
+ "redis-switchboard-mcp",
120
+ "--config",
121
+ "/Users/me/.config/redis-switchboard.config.json"
122
+ ]
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ The same file can be selected with `REDIS_MCP_CONFIG=/path/to/config.json`.
129
+
130
+ > **Note:** diagnostics are written to stderr. Never print to stdout from this process — the MCP transport owns it.
131
+
132
+ ---
133
+
134
+ <h2 align="center">⚙️ Configuration</h2>
135
+
136
+ <h3 align="center">🔀 Precedence</h3>
137
+
138
+ 1. Command-line flags (`--config`, `--db`, ...)
139
+ 2. Environment variables
140
+ 3. Defaults
141
+
142
+ `--config` selects a JSON config file. `--name`/`--db` apply only to the single-instance environment form; edit the JSON file for per-instance values.
143
+
144
+ <h3 align="center">🎛️ Command-line options</h3>
145
+
146
+ <p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
147
+
148
+ | Option | Required | Description |
149
+ | :---: | :---: | :---: |
150
+ | `--config <path>` | 🔶 Conditional | JSON config file describing one or more Redis instances |
151
+ | `--name <name>` | ⚪ Optional | Instance name (default: `default`) |
152
+ | `--db <n>` | ⚪ Optional | Default logical database, 0-255 (default: 0) |
153
+ | `--mode <mode>` | ⚪ Optional | Override permission mode for all instances: `readonly` or `readwrite` |
154
+ | `-h`, `--help` | ⚪ Optional | Show help |
155
+ | `-v`, `--version` | ⚪ Optional | Show the version |
156
+
157
+
158
+ <h3 align="center">🌱 Environment variables (single instance)</h3>
159
+
160
+ <p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
161
+
162
+ | Variable | Required | Default | Description |
163
+ | :---: | :---: | :---: | :---: |
164
+ | `REDIS_MCP_CONFIG` | 🔶 Conditional | — | Path to a multi-instance JSON config file |
165
+ | `REDIS_HOST` | ✅ Required | — | Redis host |
166
+ | `REDIS_PORT` | ✅ Required | — | Redis port |
167
+ | `REDIS_USERNAME` | ⚪ Optional | — | ACL username |
168
+ | `REDIS_PASSWORD` | 🔶 Conditional | — | Password (required when Redis requires authentication) |
169
+ | `REDIS_DB` | ⚪ Optional | `0` | Default logical database when `db` is omitted |
170
+ | `REDIS_TLS` | ⚪ Optional | `false` | Enable TLS |
171
+ | `REDIS_TLS_REJECT_UNAUTHORIZED` | ⚪ Optional | `true` | Verify the server certificate |
172
+ | `REDIS_MODE` | ✅ Required | — | Permission mode: `readonly` or `readwrite` (required for env config unless `--mode` is used) |
173
+ | `REDIS_ALLOWED_DBS` | ⚪ Optional | — | Comma-separated allowlist, e.g. `0,2,4` |
174
+ | `REDIS_ALLOW_SCRIPTING` | ⚪ Optional | `false` | Allow `EVAL`/`EVALSHA`/`FCALL` (see security caveat) |
175
+ | `REDIS_MAX_CONNECTIONS` | ⚪ Optional | `32` | Per-instance connection cache limit (1-256) |
176
+ | `REDIS_CONNECT_TIMEOUT_MS` | ⚪ Optional | `5000` | TCP/connect timeout |
177
+ | `REDIS_COMMAND_TIMEOUT_MS` | ⚪ Optional | `15000` | Per-command timeout; `0` disables it |
178
+ | `REDIS_MAX_RESULT_ITEMS` | ⚪ Optional | `200` | Per-array/object item cap in replies |
179
+ | `REDIS_MAX_RESULT_CHARS` | ⚪ Optional | `4000` | Per-string cap in replies |
180
+ | `REDIS_MAX_RESULT_DEPTH` | ⚪ Optional | `4` | Maximum nested container depth |
181
+ | `REDIS_MAX_FETCH_ITEMS` | ⚪ Optional | `1000` | Max items one `execute` bulk read may fetch |
182
+ | `REDIS_MAX_FETCH_CHARS` | ⚪ Optional | `1000000` | Max bytes one `execute` string read may fetch |
183
+ | `REDIS_SCAN_COUNT` | ⚪ Optional | `500` | Default SCAN `COUNT` hint |
184
+ | `REDIS_MAX_SCAN_ITERATIONS` | ⚪ Optional | `200` | Maximum SCAN round trips per call |
185
+ | `REDIS_SCAN_TIME_BUDGET_MS` | ⚪ Optional | `10000` | Wall-clock budget per `scan_keys` call |
186
+
187
+ <p align="center"><strong>Connection methods:</strong> choose one of the following.</p>
188
+
189
+ | Method | Required | How |
190
+ | :---: | :---: | :---: |
191
+ | JSON config | 🔶 Conditional | Set `REDIS_MCP_CONFIG` (or `--config`) |
192
+ | Host + port | ✅ Required | Set `REDIS_HOST` and `REDIS_PORT` |
193
+
194
+ 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.
195
+
196
+ 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.
197
+
198
+ <h3 align="center">🗂️ Multi-instance JSON</h3>
199
+
200
+ ```json
201
+ {
202
+ "defaultInstance": "cache-dev",
203
+ "instances": [
204
+ {
205
+ "name": "cache-dev",
206
+ "host": "127.0.0.1",
207
+ "port": 6379,
208
+ "db": 0,
209
+ "mode": "readwrite",
210
+ "allowedDbs": [0, 2, 4],
211
+ "maxConnections": 32
212
+ },
213
+ {
214
+ "name": "cache-prod",
215
+ "host": "redis.prod.internal",
216
+ "port": 6380,
217
+ "username": "app",
218
+ "passwordEnv": "PROD_REDIS_PASSWORD",
219
+ "tls": true,
220
+ "db": 0,
221
+ "mode": "readonly",
222
+ "allowedDbs": [0],
223
+ "commandTimeoutMs": 10000,
224
+ "maxResultItems": 100,
225
+ "maxResultChars": 2000,
226
+ "maxFetchItems": 500,
227
+ "maxFetchChars": 500000
228
+ }
229
+ ]
230
+ }
231
+ ```
232
+
233
+ <p align="center"><strong>Top-level fields</strong></p>
234
+
235
+ <p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
236
+
237
+ | Field | Required | Default | Description |
238
+ | :---: | :---: | :---: | :---: |
239
+ | `instances` | ✅ Required | — | Array with at least one instance |
240
+ | `defaultInstance` | ⚪ Optional | — | Instance used when a tool call omits `instance` |
241
+ | `$schema` | ⚪ Optional | — | Ignored; useful for editor schema hints |
242
+
243
+ <p align="center"><strong>Instance fields</strong></p>
244
+
245
+ <p align="center"><strong>Legend:</strong> ✅ Required · 🔶 Conditionally required · ⚪ Optional</p>
246
+
247
+ | Field | Required | Default | Description |
248
+ | :---: | :---: | :---: | :---: |
249
+ | `name` | 🔶 Conditional | `default` | Unique instance name; must be unique when several instances are configured |
250
+ | `host` / `port` | ✅ Required | — | Connection endpoint |
251
+ | `username` | ⚪ Optional | — | ACL username |
252
+ | `password` | 🔶 Conditional | — | Inline password (required when Redis requires authentication) |
253
+ | `usernameEnv` / `passwordEnv` | ⚪ Optional | — | Read credentials from an environment variable |
254
+ | `db` | ⚪ Optional | `0` | Default logical database |
255
+ | `tls` | ⚪ Optional | `false` | Enable TLS |
256
+ | `tlsRejectUnauthorized` | ⚪ Optional | `true` | Verify the server certificate |
257
+ | `mode` | ✅ Required | — | Permission mode: `readonly` or `readwrite` |
258
+ | `allowScripting` | ⚪ Optional | `false` | Allow `EVAL`/`EVALSHA`/`FCALL` |
259
+ | `allowedDbs` | ⚪ Optional | — | Restrict which logical DBs this instance may access |
260
+ | `maxConnections` | ⚪ Optional | `32` | Cache limit for `(instance, db)` connections |
261
+ | `connectTimeoutMs` | ⚪ Optional | `5000` | TCP/connect timeout |
262
+ | `commandTimeoutMs` | ⚪ Optional | `15000` | Per-command timeout; `0` disables |
263
+ | `maxResultItems` | ⚪ Optional | `200` | Per-container item cap |
264
+ | `maxResultChars` | ⚪ Optional | `4000` | Per-string character cap |
265
+ | `maxResultDepth` | ⚪ Optional | `4` | Nested container depth limit |
266
+ | `maxFetchItems` | ⚪ Optional | `1000` | Max items one `execute` bulk read may fetch |
267
+ | `maxFetchChars` | ⚪ Optional | `1000000` | Max bytes one `execute` string read may fetch |
268
+ | `scanCount` | ⚪ Optional | `500` | Default SCAN `COUNT` hint |
269
+ | `maxScanIterations` | ⚪ Optional | `200` | SCAN round-trip cap |
270
+ | `scanTimeBudgetMs` | ⚪ Optional | `10000` | `scan_keys` wall-clock budget |
271
+
272
+ Use `passwordEnv` rather than storing a password in the file, and keep the file `chmod 600`.
273
+
274
+ ---
275
+
276
+ ## Tools
277
+
278
+ ### `execute`
279
+
280
+ Run one Redis command against a specific logical database.
281
+
282
+ ```json
283
+ { "command": "GET user:42", "db": 2 }
284
+ ```
285
+
286
+ ```json
287
+ { "command": ["HGET", "user:42", "name"], "db": 4, "instance": "cache-prod" }
288
+ ```
289
+
290
+ - `command` is a command line (quotes and backslash escapes supported) or an argument array.
291
+ - `db` selects the logical database per call. It defaults to the instance's `db`.
292
+ - `instance` is required when several instances are configured without a default.
293
+ - Write commands are rejected when the instance `mode` is `readonly`.
294
+ - `SELECT` is always rejected — pass `db` instead.
295
+ - 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.
296
+
297
+ Response:
298
+
299
+ ```json
300
+ {
301
+ "instance": "cache-dev",
302
+ "db": 2,
303
+ "command": "GET",
304
+ "argCount": 1,
305
+ "elapsedMs": 1,
306
+ "reply": "alice",
307
+ "truncated": false
308
+ }
309
+ ```
310
+
311
+ When a reply is cut, `truncated` is `true` and `notes` explains which parts were shortened.
312
+
313
+ ### `read_page`
314
+
315
+ 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.
316
+
317
+ ```json
318
+ { "kind": "list", "key": "queue", "db": 2, "offset": 0, "limit": 100 }
319
+ ```
320
+
321
+ | `kind` | Redis command | Paging |
322
+ | --- | --- | --- |
323
+ | `list` | `LRANGE key offset offset+limit-1` | `nextOffset` |
324
+ | `string` | `GETRANGE key offset offset+limit-1` (`limit` = bytes) | `nextOffset` |
325
+ | `hash` | `HSCAN key cursor COUNT limit [MATCH pattern]` | `nextCursor` |
326
+ | `set` | `SSCAN key cursor COUNT limit [MATCH pattern]` | `nextCursor` |
327
+ | `zset` | `ZRANGE key offset offset+limit-1 [WITHSCORES]` | `nextOffset` |
328
+ | `stream` | `XRANGE key start end COUNT limit` | `nextStart` |
329
+
330
+ Response (list):
331
+
332
+ ```json
333
+ {
334
+ "instance": "cache-dev",
335
+ "db": 2,
336
+ "kind": "list",
337
+ "key": "queue",
338
+ "limit": 100,
339
+ "items": ["job:1", "job:2"],
340
+ "offset": 0,
341
+ "returned": 2,
342
+ "total": 12345,
343
+ "nextOffset": 2,
344
+ "complete": false,
345
+ "outputBytes": 12
346
+ }
347
+ ```
348
+
349
+ - `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.
350
+ - `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.
351
+ - Per-item strings are truncated at `maxResultChars`; `truncatedItems` reports how many were cut. `notes` reports limit reductions and SCAN batch overflows.
352
+ - `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).
353
+ - 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`.
354
+ - 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`.
355
+ - For hash/set paging, pass `nextCursor` back as `cursor`.
356
+
357
+ ### `scan_keys`
358
+
359
+ Cursor-based `SCAN` (never `KEYS`).
360
+
361
+ ```json
362
+ { "pattern": "session:*", "db": 2, "limit": 200, "type": "hash" }
363
+ ```
364
+
365
+ Response:
366
+
367
+ ```json
368
+ {
369
+ "instance": "cache-dev",
370
+ "db": 2,
371
+ "pattern": "session:*",
372
+ "type": "hash",
373
+ "cursor": "18432",
374
+ "keys": ["session:1", "session:2"],
375
+ "returned": 2,
376
+ "limit": 200,
377
+ "complete": false,
378
+ "hasMore": true,
379
+ "scannedEntries": 412,
380
+ "iterations": 2,
381
+ "elapsedMs": 3,
382
+ "outputChars": 42,
383
+ "duplicateKeys": 0,
384
+ "truncatedKeyNames": 0,
385
+ "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."]
386
+ }
387
+ ```
388
+
389
+ Pass `cursor` back unchanged (prefer the string form) to continue. Notes:
390
+
391
+ - SCAN is not a snapshot; keys may appear, disappear or be returned twice across calls.
392
+ - Duplicates **inside one call** are removed.
393
+ - 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).
394
+ - `count` is clamped to 1,000 to keep one batch bounded; key names are capped at 4,096 characters.
395
+ - `scannedEntries` is the number of names returned by SCAN batches, not an exact count of visited keys.
396
+
397
+ ### `key_info`
398
+
399
+ Metadata only — never the value itself.
400
+
401
+ ```json
402
+ { "key": "user:42", "db": 2 }
403
+ ```
404
+
405
+ Response:
406
+
407
+ ```json
408
+ {
409
+ "instance": "cache-dev",
410
+ "db": 2,
411
+ "key": "user:42",
412
+ "exists": true,
413
+ "type": "hash",
414
+ "ttlMs": 3600000,
415
+ "persistent": false,
416
+ "memoryUsageBytes": 184,
417
+ "encoding": "listpack",
418
+ "length": 3,
419
+ "lengthUnit": "items"
420
+ }
421
+ ```
422
+
423
+ `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.
424
+
425
+ ---
426
+
427
+ ## Security model
428
+
429
+ ### Permission modes
430
+
431
+ The permission mode is an explicit enum. There is no `readOnly` boolean and no implicit default:
432
+
433
+ | Mode | Behavior |
434
+ | :---: | :---: |
435
+ | `readonly` | Only an explicit read-command whitelist is accepted; `SET`, `HSET`, `DEL`, `RESTORE`, ... are rejected. `EVAL`/`FCALL` remain rejected even if `allowScripting` is true. |
436
+ | `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. |
437
+
438
+ 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`.
439
+
440
+ `REDIS_MODE` cannot be combined with a JSON config file; set `mode` per instance instead (or use `--mode` to override all instances).
441
+
442
+ 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.
443
+
444
+ **Migration:** `readOnly` (JSON), `REDIS_READONLY` and `--readonly` were removed. Use `mode`, `REDIS_MODE` and `--mode` instead.
445
+
446
+ ### Always rejected
447
+
448
+ | Category | Examples |
449
+ | --- | --- |
450
+ | Database / connection state | `SELECT`, `MOVE`, `SWAPDB`, `COPY ... DB`, `MULTI`, `EXEC`, `WATCH`, `RESET`, `HELLO`, `AUTH`, `QUIT` |
451
+ | Pub/sub and streaming | `SUBSCRIBE`, `PSUBSCRIBE`, `MONITOR`, `SYNC`, `PSYNC` |
452
+ | Administration | `CONFIG`, `DEBUG`, `CLIENT`, `ACL`, `MODULE`, `SHUTDOWN`, `SCRIPT`, `FUNCTION` |
453
+ | Topology / replication | `CLUSTER`, `REPLICAOF`, `SLAVEOF`, `FAILOVER`, `MIGRATE`, `REPLCONF` |
454
+ | Destructive / server-wide | `FLUSHDB`, `FLUSHALL`, `SAVE`, `BGSAVE`, `BGREWRITEAOF` |
455
+ | Blocking | `BLPOP`, `BRPOP`, `BLMOVE`, `BZPOPMIN`, `WAIT`, `XREAD`, `XREADGROUP`, ... |
456
+ | Slow key enumeration | `KEYS`, raw `SCAN` — use `scan_keys` |
457
+
458
+ Blocking commands are rejected because they hold a shared per-database connection and can stall every later request on that DB.
459
+
460
+ ### Lua scripting
461
+
462
+ `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.
463
+
464
+ > 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.
465
+
466
+ ### Output and fetch bounds
467
+
468
+ - Per array/object: `maxResultItems`
469
+ - Per string/buffer: `maxResultChars`
470
+ - Nesting: `maxResultDepth`
471
+ - 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
472
+ - `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
473
+ - `MGET` is limited to 100 keys per call; `HMGET` is limited to `maxFetchItems` requested fields
474
+ - `read_page`: server-side paging with `limit` capped at 1,000 / `maxFetchItems`
475
+ - `scan_keys`: `limit`, `maxScanIterations`, `scanTimeBudgetMs`
476
+ - `maxConnections`: bounds total sockets per instance
477
+
478
+ ### Credentials
479
+
480
+ - 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.
481
+ - Prefer `passwordEnv` / `REDIS_PASSWORD` over inline JSON passwords.
482
+ - Keep a config file containing secrets at `chmod 600`.
483
+ - Error messages redact `password=...`, quoted/JSON password fields, echoed `AUTH` arguments, and any connection URL that may appear in an error.
484
+ - TLS certificate verification is on by default; `tlsRejectUnauthorized: false` disables it and should only be used for self-signed development servers.
485
+
486
+ ### Transport
487
+
488
+ 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.
489
+
490
+ ---
491
+
492
+ ## Behavior and limitations
493
+
494
+ - **No `SELECT`**: logical DB selection is per call. This is the point of the project.
495
+ - **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.
496
+ - **No transactions**: `MULTI`/`EXEC`/`WATCH` are connection-level and would leak between requests.
497
+ - **No pub/sub or `MONITOR`**: they change the connection mode.
498
+ - **No blocking commands**: they would pin a shared connection. Use non-blocking variants (`XRANGE` instead of blocking `XREAD`, etc.).
499
+ - **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.
500
+ - **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.
501
+ - **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.
502
+ - **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.
503
+ - **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.
504
+ - **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.
505
+ - **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.
506
+ - **SCAN consistency**: SCAN is incremental and not a snapshot; results can be missed or duplicated across calls.
507
+ - **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.
508
+ - **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`.
509
+
510
+ ---
511
+
512
+ ## Development
513
+
514
+ ```bash
515
+ npm ci
516
+ npm run check # typecheck src + tests, then run the test suite
517
+ npm run build # compile to dist/
518
+ npm run dev # run from src with tsx
519
+ ```
520
+
521
+ 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:
522
+
523
+ - command tokenization, blocklist and readonly policy
524
+ - reply truncation and global budgets
525
+ - environment / JSON / CLI configuration
526
+ - per-database client isolation and promise caching
527
+ - connection cache limits and error mapping
528
+ - `execute`, `read_page`, `scan_keys`, `key_info` end-to-end over real ioredis
529
+ - fetch guards for `GET` / `LRANGE` / `HGETALL` / `SMEMBERS` / `ZRANGE` / `XRANGE` / random members / set operations
530
+ - MCP handshake and `tools/list` over stdio
531
+
532
+ Project layout:
533
+
534
+ ```text
535
+ src/
536
+ index.ts CLI entry and stdio transport
537
+ lib.ts Side-effect-free library entry
538
+ server.ts MCP server + tool registration
539
+ config.ts env / JSON / CLI configuration
540
+ commands.ts tokenizer, blocklist, readonly whitelist
541
+ truncate.ts bounded reply serialization
542
+ redis-manager.ts per-(instance, db) client cache
543
+ fetch-guard.ts preflight size guards for execute bulk reads
544
+ tools/ execute, read_page, scan_keys, key_info handlers
545
+ test/ unit, integration and MCP handshake tests
546
+ ```
547
+
548
+ ---
549
+
550
+ ## License
551
+
552
+ MIT