@shibbirweb/mcp-db-read-only 0.2.0 → 1.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.
@@ -2,105 +2,48 @@
2
2
  This file is the Docker Hub description, published by
3
3
  .github/workflows/dockerhub-description.yml via `readme-filepath`.
4
4
 
5
- It exists because Docker Hub does NOT render mermaid: a ```mermaid block
6
- shows up as raw source. It also does not resolve relative links, so every
7
- link here must be absolute.
8
-
9
- Keep it in sync with README.md. Same content, with ASCII diagrams instead of
10
- mermaid and the contributor sections reduced to pointers.
5
+ Docker Hub does not resolve relative links, so every link here is absolute.
6
+ Keep it in sync with README.md: the same content, plus the tags list below.
11
7
  -->
12
8
 
13
9
  # mcp-db-read-only
14
10
 
15
11
  [![CI](https://github.com/shibbirweb/mcp-db-read-only/actions/workflows/ci.yml/badge.svg)](https://github.com/shibbirweb/mcp-db-read-only/actions/workflows/ci.yml)
16
12
  [![npm](https://img.shields.io/npm/v/%40shibbirweb%2Fmcp-db-read-only?label=npm&color=cb3837)](https://www.npmjs.com/package/@shibbirweb/mcp-db-read-only)
13
+ [![npm downloads](https://img.shields.io/npm/dm/%40shibbirweb%2Fmcp-db-read-only?style=flat&label=npm%20downloads)](https://www.npmjs.com/package/@shibbirweb/mcp-db-read-only)
17
14
  [![Docker Hub](https://img.shields.io/docker/v/shibbirweb/mcp-db-read-only?label=docker%20hub&sort=semver)](https://hub.docker.com/r/shibbirweb/mcp-db-read-only)
18
- [![Image size](https://img.shields.io/docker/image-size/shibbirweb/mcp-db-read-only/latest?style=flat&label=image%20size)](https://hub.docker.com/r/shibbirweb/mcp-db-read-only/tags)
15
+ [![Docker pulls](https://img.shields.io/docker/pulls/shibbirweb/mcp-db-read-only?style=flat)](https://hub.docker.com/r/shibbirweb/mcp-db-read-only)
19
16
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shibbirweb/mcp-db-read-only/blob/master/LICENSE)
20
17
 
21
- **Source and full documentation: [github.com/shibbirweb/mcp-db-read-only](https://github.com/shibbirweb/mcp-db-read-only)**
22
-
23
- An [MCP](https://modelcontextprotocol.io) server that gives an AI assistant **read-only** access to your databases, whichever kind they are, and lets it **switch database, server, credentials and even engine mid-conversation without restarting the client**.
24
-
25
- | Engine | URL scheme | Queried with |
26
- | --- | --- | --- |
27
- | MySQL, MariaDB | `mysql://`, `mariadb://` | `run_query` (SQL) |
28
- | PostgreSQL (and wire-compatible) | `postgres://`, `postgresql://` | `run_query` (SQL) |
29
- | SQLite | `sqlite:///path/to/file.db` | `run_query` (SQL) |
30
- | SQL Server, Azure SQL | `mssql://`, `sqlserver://` | `run_query` (T-SQL) |
31
- | ClickHouse | `clickhouse://`, `clickhouse+https://` | `run_query` (SQL) |
32
- | MongoDB | `mongodb://`, `mongodb+srv://` | `find_documents`, `aggregate`, `count_documents`, `distinct_values` |
33
- | Redis (and Valkey, KeyDB) | `redis://`, `rediss://` | `redis_command` |
34
- | Elasticsearch, OpenSearch | `elasticsearch://`, `opensearch://`, `+https` variants | `search` |
18
+ Let your AI assistant **look at your databases without being able to change them.**
35
19
 
36
- The browse tools (`list_tables`, `describe_table`, `get_table_sample`, ...) work on every engine, in that engine's terms: tables, collections, keys or indices.
20
+ Point it at MySQL, PostgreSQL, SQLite, SQL Server, ClickHouse, MongoDB, Redis or Elasticsearch, then just ask questions in plain words: *"how many users signed up this week?"*, *"what's in the orders table?"*, *"which Redis keys hold sessions?"*. It can read anything you give it access to, and it cannot write, update or delete anything.
37
21
 
38
- Runs from npm with `npx`, or entirely in Docker with nothing installed on your machine.
22
+ - **Every popular database, one server.** Switch between them in the middle of a conversation.
23
+ - **Read-only, twice over.** Every query is checked before it is sent, and the database itself is also told to refuse writes.
24
+ - **No restart to switch.** Change database, server or engine by asking.
25
+ - **Optional logging**, with a live page in your browser that shows every query as it happens.
39
26
 
40
27
  ```text
41
- +--------------------------------------+
42
- | AI assistant |
43
- | Claude Desktop / Claude Code |
44
- +------------------+-------------------+
45
- |
46
- MCP over stdio
47
- |
48
- +------------------v-------------------+
49
- | mcp-db-read-only |
50
- | one container, whole session |
51
- +------------------+-------------------+
52
- |
53
- one driver per target
54
- |
55
- +-------------+-------------+-+-----------+-----------------+
56
- | | | | |
57
- v v v v v
58
- ( PostgreSQL ) ( MySQL ) ( MongoDB ) ( Redis ) ( anything reached
59
- app legacy events cache with `connect` )
28
+ You --ask in plain words--> Your AI assistant --tool call--> mcp-db-read-only --read-only query--> Your databases
29
+ mcp-db-read-only <--rows, documents, keys--
60
30
  ```
61
31
 
62
- The server lives for the whole session, so the active connection is just state inside it. Switching selects a different driver rather than reconnecting, and switching back reuses a warm one.
32
+ Works with Claude Desktop, Claude Code, and any other [MCP](https://modelcontextprotocol.io) client.
63
33
 
64
- ---
34
+ **Source and full documentation: [github.com/shibbirweb/mcp-db-read-only](https://github.com/shibbirweb/mcp-db-read-only)**
65
35
 
66
36
  ## Supported tags
67
37
 
68
- `0.2.0`, `0.2`, `0`, `latest`, built for `linux/amd64` and `linux/arm64`.
38
+ `1.1.0`, `1.1`, `1`, `latest`, built for `linux/amd64` and `linux/arm64`.
69
39
 
70
40
  ---
71
41
 
72
42
  ## Quick start
73
43
 
74
- ### npm
75
-
76
- ```bash
77
- DB_URL='postgres://readonly:secret@127.0.0.1:5432/my_database' npx -y @shibbirweb/mcp-db-read-only
78
- ```
79
-
80
- Requires Node 22.13 or newer. There is no container in the way, so `127.0.0.1` means what you expect.
81
-
82
- ### Docker
83
-
84
- ```bash
85
- docker run -i --rm \
86
- --add-host host.docker.internal:host-gateway \
87
- -e DB_URL='postgres://readonly:secret@host.docker.internal:5432/my_database' \
88
- shibbirweb/mcp-db-read-only
89
- ```
44
+ **1. Have Node.js 22.13 or newer** (`node --version`), or Docker.
90
45
 
91
- Use `host.docker.internal` to reach a database on the same machine as Docker. Inside the container, `localhost` means the container itself.
92
-
93
- For SQLite in Docker, mount the file's directory read-only and point at the path inside the container:
94
-
95
- ```bash
96
- docker run -i --rm -v "$PWD/data:/data:ro" -e DB_URL='sqlite:///data/app.db' shibbirweb/mcp-db-read-only
97
- ```
98
-
99
- The container is the more isolated of the two: the server runs with only what the image and the environment give it. Over npm it runs directly on your machine with your user's access. Both enforce the same read-only guarantees.
100
-
101
- ### Claude Desktop
102
-
103
- Add to `claude_desktop_config.json`:
46
+ **2. Add the server to your client.** For Claude Desktop, edit `claude_desktop_config.json` (Settings, Developer, Edit Config). For Claude Code, create `.mcp.json` in your project:
104
47
 
105
48
  ```json
106
49
  {
@@ -109,322 +52,324 @@ Add to `claude_desktop_config.json`:
109
52
  "command": "npx",
110
53
  "args": ["-y", "@shibbirweb/mcp-db-read-only"],
111
54
  "env": {
112
- "DB_PROFILES": "{\"app\": \"postgres://readonly@127.0.0.1/app\", \"cache\": \"redis://127.0.0.1:6379/0\"}",
113
- "DB_DEFAULT_PROFILE": "app"
55
+ "DB_URL": "postgres://readonly:secret@localhost:5432/myapp"
114
56
  }
115
57
  }
116
58
  }
117
59
  }
118
60
  ```
119
61
 
120
- Or the same server in Docker:
62
+ Replace the `DB_URL` with your own database. The examples below show one for every kind.
121
63
 
122
- ```json
123
- {
124
- "mcpServers": {
125
- "databases": {
126
- "command": "docker",
127
- "args": [
128
- "run", "-i", "--rm",
129
- "--add-host", "host.docker.internal:host-gateway",
130
- "-e", "DB_URL=postgres://readonly:secret@host.docker.internal:5432/app",
131
- "shibbirweb/mcp-db-read-only"
132
- ]
133
- }
134
- }
135
- }
136
- ```
64
+ **3. Restart the client once, and ask away:**
137
65
 
138
- ### Claude Code
66
+ > "What tables are in my database?"
67
+ > "Show me the 5 newest orders."
68
+ > "How many customers are in each country?"
139
69
 
140
- The same shape, in `.mcp.json` at your project root. Either form above works.
70
+ That's it. You never need to restart again to change database; just ask the assistant to switch.
141
71
 
142
- Restart the client once. After that you never need to restart it to change database.
72
+ ---
143
73
 
144
- > Credentials in these files sit on disk in plain text. Prefer read-only database accounts, and keep the file out of version control. See [Security](https://github.com/shibbirweb/mcp-db-read-only#security).
74
+ ## Examples for each database
145
75
 
146
- ### Coming from mcp-mysql-read-only
76
+ Each database has a URL **format**, then a real **example** to copy and change. Put the finished URL in `DB_URL`.
147
77
 
148
- This server reads the old `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `MYSQL_PROFILES` and `MYSQL_DEFAULT_PROFILE` unchanged, so swapping the image or package name is enough. Tool names are the same too; the one change is that `connect` now takes a URL.
78
+ Replace each `[PLACEHOLDER]` with your own value:
149
79
 
150
- ---
80
+ | Placeholder | What to put there |
81
+ | --- | --- |
82
+ | `[USER]` | The database user name |
83
+ | `[PASSWORD]` | That user's password |
84
+ | `[HOST]` | The server's address, e.g. `localhost` or `db.example.com` |
85
+ | `[PORT]` | The server's port. Optional: leave out `:[PORT]` to use the usual one shown for each database |
86
+ | `[DATABASE]` | The database name. Optional for most: leave it out and ask the assistant to list them |
151
87
 
152
- ## Switching connections
88
+ No password? Leave out `:[PASSWORD]`. No user either? Leave out `[USER]:[PASSWORD]@` entirely.
153
89
 
154
- Just ask. These map onto the connection tools:
90
+ ### MySQL and MariaDB
155
91
 
156
- > "switch to the staging database"
157
- > "what collections are in the events database?"
158
- > "connect to the Redis on 10.0.0.5 and show me the session keys"
92
+ Format (usual port 3306):
159
93
 
160
- | Want | Restart? |
161
- | --- | --- |
162
- | Another database on the same server | No |
163
- | Another named profile, on any engine | No |
164
- | A different server, credentials or engine | No |
165
- | A new permanent profile in `DB_PROFILES` | Yes, once |
94
+ ```text
95
+ mysql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
96
+ mariadb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
97
+ ```
98
+
99
+ Example:
166
100
 
167
101
  ```text
168
- You Assistant MCP server PostgreSQL MongoDB
169
- | "signups?" | | | |
170
- |-------------->| run_query(SELECT ...) | | |
171
- | |------------------------->| read-only transaction| |
172
- | | |--------------------->| |
173
- | |<-------------------------|<------ 4821 ---------| |
174
- | "opened app?" | | |
175
- |-------------->| use_connection(events) | connect + ping |
176
- | |------------------------->|------------------------------>|
177
- | | | verified, switch committed |
178
- | | count_documents(...) | |
179
- | |------------------------->|------------------------------>|
180
- |<-- 3907 ------|<-------------------------|<------------ 3907 ------------|
102
+ mysql://readonly:secret@localhost:3306/shop
181
103
  ```
182
104
 
183
- A switch that fails verification is never committed, so the previous connection stays active and the session keeps working.
105
+ > "List the tables in shop." · "Describe the orders table." · "What were last month's top 10 products by revenue?"
184
106
 
185
- ### Named profiles
107
+ ### PostgreSQL
186
108
 
187
- Define several connections up front with `DB_PROFILES`, a JSON object whose values are URLs, or objects with a separate password:
109
+ Format (usual port 5432). Add `?sslmode=require` to use TLS:
188
110
 
189
- ```json
190
- {
191
- "app": "postgres://readonly@db.internal:5432/app",
192
- "legacy": "mysql://readonly@legacy.internal/shop",
193
- "events": { "url": "mongodb://reader@mongo.internal/events", "password": "p@ss/w#rd" },
194
- "cache": "redis://cache.internal:6379/0",
195
- "logs": "elasticsearch+https://reader@logs.internal:9200",
196
- "reports": "sqlite:///data/reports.db"
197
- }
111
+ ```text
112
+ postgres://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
198
113
  ```
199
114
 
200
- The object form exists because a password inside a URL must be percent-encoded, and one containing `@`, `/` or `#` otherwise splits the URL in the wrong place. A profile that fails to parse is skipped with a warning rather than taking the server down.
115
+ Example:
201
116
 
202
- ### Reaching somewhere not in the profiles
117
+ ```text
118
+ postgres://readonly:secret@localhost:5432/myapp
119
+ ```
203
120
 
204
- The `connect` tool takes a URL (and optionally a separate password) at runtime and keeps it for the rest of the session under an alias. Nothing is written to disk, and no restart is involved.
121
+ > "Which tables are in the reporting schema?" · "Show the foreign keys on invoices." · "Count signups per day this week."
205
122
 
206
- ### URL details
123
+ ### SQLite
207
124
 
208
- | Engine | Notes |
209
- | --- | --- |
210
- | MySQL | `?ssl=true` requires TLS; `?ssl-mode=VERIFY_IDENTITY` also checks the certificate |
211
- | PostgreSQL | `?sslmode=require`, `verify-ca` or `verify-full`, as in libpq |
212
- | SQLite | `sqlite:///absolute/path.db`; a relative path is resolved once, at startup |
213
- | SQL Server | Encrypted by default. `?trustServerCertificate=true` for self-signed development servers; `?applicationIntent=ReadOnly` routes to a readable secondary |
214
- | ClickHouse | The HTTP interface: port 8123, or 8443 with `clickhouse+https` |
215
- | MongoDB | Replica sets as `mongodb://a:27017,b:27017/db?replicaSet=rs0`; any driver option passes through the query string |
216
- | Redis | The path is the database number: `redis://host:6379/3` |
217
- | Elasticsearch | `?api_key=...` authenticates with an API key; it is treated as a secret and never displayed |
125
+ Format (three slashes, then the full path to the file):
218
126
 
219
- ---
127
+ ```text
128
+ sqlite:///[PATH_TO_FILE]
129
+ ```
220
130
 
221
- ## Tools
131
+ Example:
222
132
 
223
- ### Connection
133
+ ```text
134
+ sqlite:///Users/me/data/app.db
135
+ ```
224
136
 
225
- | Tool | Purpose |
226
- | --- | --- |
227
- | `current_connection` | Which engine, server and database is active |
228
- | `list_connections` | Available profiles and their engines, `*` marks the active one |
229
- | `list_databases` | Databases on the connected server |
230
- | `use_database` | Switch database on the current server |
231
- | `use_connection` | Switch to a named profile, optional `database` override |
232
- | `connect` | Open any server from a URL, optional `alias` |
137
+ The file is opened read-only.
233
138
 
234
- ### Browsing, on every engine
139
+ > "What tables does this file have?" · "Show 10 rows from notes."
235
140
 
236
- | Tool | SQL engines | MongoDB | Redis | Elasticsearch |
237
- | --- | --- | --- | --- | --- |
238
- | `list_tables` | tables and views | collections | keys, by SCAN | indices |
239
- | `describe_table` | columns | fields inferred from 100 sampled documents | type, TTL, length | mapping |
240
- | `get_table_indexes` | indexes | indexes | n/a | n/a |
241
- | `get_foreign_keys` | foreign keys (not ClickHouse) | n/a | n/a | n/a |
242
- | `get_table_sample` | up to 50 rows | up to 50 documents | the start of the value | up to 50 hits |
141
+ ### SQL Server (and Azure SQL)
243
142
 
244
- `list_tables` takes an optional glob `pattern`, such as `user*`, which is how you browse a Redis instance with millions of keys.
143
+ Format (usual port 1433). Add `?trustServerCertificate=true` for a local server with a self-signed certificate:
245
144
 
246
- ### Querying, per engine family
145
+ ```text
146
+ mssql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
147
+ ```
247
148
 
248
- | Tool | Engine | Accepts |
249
- | --- | --- | --- |
250
- | `run_query` | SQL engines | One read-only statement in the engine's own dialect |
251
- | `find_documents` | MongoDB | Filter, projection, sort, limit and skip, as Extended JSON |
252
- | `aggregate` | MongoDB | A pipeline, without `$out` or `$merge` |
253
- | `count_documents` | MongoDB | A filter |
254
- | `distinct_values` | MongoDB | A field and an optional filter |
255
- | `search` | Elasticsearch, OpenSearch | A Query DSL body; `size: 0` with `track_total_hits` counts |
256
- | `redis_command` | Redis | One read-only command and its arguments |
149
+ Example:
257
150
 
258
- Every tool is advertised all the time, since MCP fixes the tool list at startup while the active engine can change. Calling one against the wrong engine says which tools fit instead.
151
+ ```text
152
+ mssql://readonly:secret@localhost:1433/Sales?trustServerCertificate=true
153
+ ```
259
154
 
260
- Every reading tool also accepts an optional `database`, applied to that call only, leaving the active connection alone.
155
+ > "List the tables in Sales." · "Show the top 5 customers by order total." (SQL Server uses `TOP 5`, not `LIMIT`; the assistant knows.)
261
156
 
262
- ---
157
+ ### ClickHouse
158
+
159
+ Format (usual port 8123, or 8443 with `clickhouse+https`):
160
+
161
+ ```text
162
+ clickhouse://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
163
+ clickhouse+https://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
164
+ ```
263
165
 
264
- ## Configuration
166
+ Example:
265
167
 
266
- | Variable | Default | Purpose |
267
- | --- | --- | --- |
268
- | `DB_URL` | none | One connection, registered as the profile `default` |
269
- | `DB_PASSWORD` | none | Password for `DB_URL`, so it need not be encoded into the URL |
270
- | `DB_PROFILES` | none | JSON object of named profiles |
271
- | `DB_DEFAULT_PROFILE` | none | Which profile starts active |
272
- | `DB_QUERY_TIMEOUT_MS` | `30000` | Statement timeout, enforced by each server where it can be |
273
- | `DB_CONNECT_TIMEOUT_MS` | `10000` | Connection timeout |
274
- | `DB_LOG` | off | `true` logs every tool call to stderr. See [Call logging](https://github.com/shibbirweb/mcp-db-read-only#call-logging) |
275
- | `DB_LOG_FILE` | none | Log every tool call to this file instead |
276
- | `DB_LOG_DIR` | none | Save every tool call as its own JSON file in this folder, permanently |
277
- | `DB_LOG_FORMAT` | `pretty` | `pretty` or `json` |
278
- | `DB_LOG_PORT` | none | Serve a live log viewer in the browser on this port |
279
- | `DB_LOG_HISTORY` | `500` | Entries the viewer keeps in memory when there is no `DB_LOG_DIR` |
280
- | `MYSQL_*` | | The legacy MySQL-only variables, read unchanged. See above |
168
+ ```text
169
+ clickhouse://reader:secret@localhost:8123/analytics
170
+ ```
281
171
 
282
- None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
172
+ > "How many events per hour did we have yesterday?" · "What is the sorting key of the events table?"
283
173
 
284
- Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names a real profile, else `default`, else the first one defined.
174
+ ### MongoDB
285
175
 
286
- ---
176
+ Format (usual port 27017). Use `mongodb+srv` for MongoDB Atlas, with no port:
287
177
 
288
- ## Call logging
178
+ ```text
179
+ mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]?authSource=admin
180
+ mongodb+srv://[USER]:[PASSWORD]@[CLUSTER_HOST]/[DATABASE]
181
+ ```
289
182
 
290
- Off by default. Turn it on to see every tool call the assistant makes: its input, each statement the drivers sent to the database, and the full output.
183
+ Example:
291
184
 
292
- | Variable | Effect |
293
- | --- | --- |
294
- | `DB_LOG=true` | Log to stderr, which your MCP client keeps (Claude Code shows it in its MCP logs) |
295
- | `DB_LOG_FILE=/path/calls.log` | Log to that file instead, appended to, created readable by you only. Implies `DB_LOG` |
296
- | `DB_LOG_DIR=/path/folder` | Save every entry as its own JSON file in that folder, permanently. Implies logging on its own, without also writing text |
297
- | `DB_LOG_FORMAT=json` | One JSON object per line, for `jq` or a log shipper. The default is `pretty` |
185
+ ```text
186
+ mongodb://reader:secret@localhost:27017/myapp?authSource=admin
187
+ ```
188
+
189
+ > "What collections are in myapp?" · "What fields do documents in users have?" · "Find the 5 most recent orders over 100." · "Count users by country."
190
+
191
+ ### Redis (and Valkey, KeyDB)
298
192
 
299
- A pretty entry:
193
+ Format (usual port 6379). `[DB_NUMBER]` is the database number, 0 if left out; `rediss` means TLS:
300
194
 
301
195
  ```text
302
- ┌─ #3 run_query · ok · 38 ms · 2026-09-25T10:14:03.221Z
303
- │ connection dev (MySQL) mysql://reader@127.0.0.1:3306/app
304
- │ input
305
- │ {
306
- │ "query": "SELECT COUNT(*) AS n FROM members"
307
- │ }
308
- │ statements (1)
309
- │ 1. MySQL · 12 ms · 1 row
310
- │ SELECT COUNT(*) AS n FROM members
311
- │ output
312
- │ [
313
- │ {
314
- │ "n": 17440
315
- │ }
316
- │ ]
317
- └─
196
+ redis://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
197
+ rediss://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
318
198
  ```
319
199
 
320
- - **Everything is logged, including the full output of every call.** Treat a log file like the data it contains.
321
- - **Credentials never are.** A `password` argument, the password inside a connection URL, and secret-looking URL options such as `api_key` are always written as `***`, with no way to turn that off.
322
- - **Statements** include the ones the server sends on its own behalf: PostgreSQL's `BEGIN READ ONLY` and `ROLLBACK`, Redis's `COMMAND INFO` checks, the catalog queries behind `describe_table`. Each shows its duration and outcome (a row count, or the error); the data itself is in the call's output. A statement sent outside any call, such as MySQL's per-connection setup, gets an entry of its own.
323
- - An entry is written when its call finishes, and numbered when it starts, so calls handled concurrently can appear out of numeric order.
324
- - With `DB_LOG_DIR`, each entry is its own file in a folder per UTC day, e.g. `2026-09-25/103014-221Z_p72440_c000012_run_query_ok.json`, holding the full record as pretty JSON: time, pid, sequence, tool and outcome are in the name, so `ls` and `grep` work without opening anything. Files and folders are readable by you only. Nothing is ever deleted or rotated; archive or remove old day folders yourself. Every copy of the server can share one folder, since names never collide.
325
- - Logging can never break a call. If the log cannot be written (say, the disk is full), the call still succeeds, one warning is printed, and logging stops.
200
+ Example:
326
201
 
327
- In Docker, a log file or folder must be on a mounted volume to outlive the container: `-v "$PWD/logs:/logs" -e DB_LOG_DIR=/logs`. `DB_LOG=true` needs no mount.
202
+ ```text
203
+ redis://localhost:6379/0
204
+ ```
328
205
 
329
- ### Live viewer in the browser
206
+ > "Which keys start with session:?" · "What's inside user:42?" · "How long until cache:home expires?"
330
207
 
331
- Add `DB_LOG_PORT` to watch calls arrive in a browser page, updating the moment each one finishes:
208
+ ### Elasticsearch and OpenSearch
332
209
 
333
- ```bash
334
- DB_LOG=true DB_LOG_PORT=4800 npx -y @shibbirweb/mcp-db-read-only
335
- # then open http://127.0.0.1:4800/
210
+ Format (usual port 9200, no database). Add `+https` for TLS, or `?api_key=[API_KEY]` instead of a user and password:
211
+
212
+ ```text
213
+ elasticsearch://[USER]:[PASSWORD]@[HOST]:[PORT]
214
+ opensearch://[USER]:[PASSWORD]@[HOST]:[PORT]
336
215
  ```
337
216
 
338
- | Variable | Default | Effect |
339
- | --- | --- | --- |
340
- | `DB_LOG_PORT` | none | Serve the live viewer on this port. Needs `DB_LOG_DIR`, `DB_LOG` or `DB_LOG_FILE` as well; alone it only prints a warning |
341
- | `DB_LOG_HISTORY` | `500` | Without `DB_LOG_DIR`, how many recent entries the viewer keeps in memory. `0` keeps none |
217
+ Example:
218
+
219
+ ```text
220
+ elasticsearch+https://elastic:secret@search.example.com:9200
221
+ ```
342
222
 
343
- The page lists calls newest first, **20 per page by default**, with page controls and a choice of 10, 20, 30 or 50 per page (remembered in your browser). Each call is a card (tool, status, duration, connection, and which client and process made it) that expands to its input, statements and output, every block with its own copy icon. Filters (text, tool, failures only) apply across everything logged, not just the page shown. Page 1 updates itself as calls arrive; on any other page a "new entries" button appears instead, so the page you are reading does not shift. It follows your system's light or dark theme and loads nothing from the internet.
223
+ > "What indices do we have?" · "Find error logs from the last hour." · "How many documents are in logs-2026.09?"
344
224
 
345
- With `DB_LOG_DIR` the viewer reads the folder, so it shows everything ever saved there, across restarts, **including calls made by other copies of the server** sharing the folder (Claude Desktop runs several). Without it, it shows this copy's last `DB_LOG_HISTORY` calls from memory.
225
+ Detailed notes for every database, including how to create a read-only account, are in the [Databases guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Databases).
346
226
 
347
- > **The viewer has no access control and listens on every network interface (`0.0.0.0`).** Anyone who can reach the port, including other devices on the same network, can read every query and every result in the log. Use it on a network you trust, or block the port at your firewall. The server prints a reminder of this when the viewer starts.
227
+ ---
348
228
 
349
- With no `DB_LOG_PORT`, nothing listens on any port.
229
+ ## Using several databases
350
230
 
351
- The viewer takes its port on the **first tool call**, not at startup. MCP clients such as Claude Desktop start one copy of the server per chat surface, and most copies are never used; binding lazily means the copy your chat is using gets the port, and idle copies hold none (they open no database connections either until used).
231
+ Give each one a name with `DB_PROFILES`, and switch by asking ("switch to legacy", "use the cache"):
352
232
 
353
- If the port is taken when a call arrives (another chat is already using the viewer, say), the call still works, and its result carries one extra line saying which process holds the port: free it, or set `DB_LOG_PORT` to another one. Every later call retries, so once the port is free the viewer comes up on the next call and says so. `current_connection` always shows the viewer's state.
233
+ ```json
234
+ "env": {
235
+ "DB_PROFILES": "{\"app\": \"postgres://readonly@localhost/app\", \"legacy\": \"mysql://readonly@localhost/shop\", \"cache\": \"redis://localhost:6379/0\"}",
236
+ "DB_DEFAULT_PROFILE": "app"
237
+ }
238
+ ```
354
239
 
355
- In Docker, publish the port as well: `-p 4800:4800 -e DB_LOG=true -e DB_LOG_PORT=4800`. Publishing it as `-p 127.0.0.1:4800:4800` keeps it reachable from this machine only.
240
+ You can also connect to a database you didn't list, mid-conversation: *"connect to postgres://readonly@10.0.0.5/reports"*.
241
+
242
+ **Password with special characters** (`@`, `/`, `#`)? Give it separately instead of inside the URL: `DB_PASSWORD` next to `DB_URL`, or `{"url": "...", "password": "..."}` inside `DB_PROFILES`.
356
243
 
357
244
  ---
358
245
 
359
- ## Security
246
+ ## Running with Docker
247
+
248
+ Nothing to install but Docker:
360
249
 
361
- Every engine is kept read-only by **two independent layers**, so a hole in one is not automatically a write. The first layer runs before any connection is used; the second is enforced by the database server itself wherever the engine offers a way, and structurally where it does not.
250
+ ```json
251
+ {
252
+ "mcpServers": {
253
+ "databases": {
254
+ "command": "docker",
255
+ "args": [
256
+ "run", "-i", "--rm",
257
+ "--add-host", "host.docker.internal:host-gateway",
258
+ "-e", "DB_URL=postgres://readonly:secret@host.docker.internal:5432/myapp",
259
+ "shibbirweb/mcp-db-read-only"
260
+ ]
261
+ }
262
+ }
263
+ }
264
+ ```
362
265
 
363
- | Engine | Layer one, in this server | Layer two |
364
- | --- | --- | --- |
365
- | MySQL, MariaDB | SQL validator | `SET SESSION TRANSACTION READ ONLY` on every connection; the driver cannot send a second statement |
366
- | PostgreSQL | SQL validator, dialect-aware (dollar quotes, `E''` strings) | Every statement runs in a `READ ONLY` transaction that is always rolled back; extended query protocol, one statement only |
367
- | SQLite | SQL validator | The file is opened read-only by SQLite; extensions disabled |
368
- | SQL Server | SQL validator, scanning every statement since T-SQL needs no separators | Every batch runs in a transaction that is always rolled back |
369
- | ClickHouse | SQL validator, refusing table functions that reach outside the server | ClickHouse's own `readonly` setting on every query |
370
- | MongoDB | Operator denylist: `$out`, `$merge`, `$function`, `$where` anywhere | Stage allowlist in the driver, which only ever calls read operations |
371
- | Redis | Command allowlist | The server's own `COMMAND INFO` flags: a command is sent only if Redis itself calls it read-only |
372
- | Elasticsearch | Search body allowlist; index names cannot address an API | The driver can only reach fixed read endpoints |
266
+ Inside Docker, use `host.docker.internal` instead of `localhost` to reach a database on your own computer. For SQLite, mount the folder: add `"-v", "/Users/me/data:/data:ro"` and use `sqlite:///data/app.db`.
373
267
 
374
- The SQL validator lexes each dialect exactly as the server will: string literals, quoted identifiers and comments are blanked before any rule looks at the statement, so a keyword or semicolon inside a literal is never mistaken for SQL. Constructs it cannot be certain the server reads the same way, such as nested block comments or MySQL's executable `/*! */` comments, are refused rather than guessed at. Only the dialect's read statements may lead (`SELECT`, `WITH`, and `SHOW`, `DESCRIBE` or `EXPLAIN` where they exist). Functions that write files, reach other servers or run SQL hidden in a string (`INTO OUTFILE`, `lo_export`, `dblink`, `OPENROWSET`, ClickHouse's `url()` and `file()`) are blocked.
268
+ ---
375
269
 
376
- Integration tests prove layer two separately: they send writes straight to each driver, bypassing every validator, and assert the server refused or undid them.
270
+ ## Watching what the assistant does
377
271
 
378
- ### What this is not
272
+ Turn on logging to keep a record of every query the assistant runs, and see them live in your browser.
379
273
 
380
- **This is a guard, not a permission system.** It stops an assistant from writing through *this* server. It does not stop anyone holding the same credentials from writing through any other client.
274
+ ```text
275
+ The assistant writes a query
276
+ |
277
+ v
278
+ 1. Checked by this server: only a read?
279
+ no --> Refused, with the reason
280
+ yes --> 2. Sent to the database in read-only mode
281
+ a read --> The answer
282
+ a write that slipped through --> Refused by the database
283
+ ```
381
284
 
382
- **Point it at read-only accounts.** This is the real protection, and on MongoDB and Elasticsearch, whose servers have no read-only session mode, it is the only server-side one:
285
+ **1. Save logs to a folder** by adding this to the server's `env`:
383
286
 
384
- ```sql
385
- -- MySQL
386
- CREATE USER 'readonly'@'%' IDENTIFIED BY '...'; GRANT SELECT ON app.* TO 'readonly'@'%';
387
- -- PostgreSQL
388
- CREATE ROLE readonly LOGIN PASSWORD '...'; GRANT pg_read_all_data TO readonly;
287
+ ```json
288
+ "DB_LOG_DIR": "/Users/me/Library/Logs/mcp-db-read-only"
389
289
  ```
390
290
 
391
- ```js
392
- // MongoDB
393
- db.createUser({ user: "reader", pwd: "...", roles: [{ role: "read", db: "app" }] });
291
+ Each query is saved as its own file, one folder per day. Nothing is ever deleted automatically.
292
+
293
+ **2. Open the viewer** in a terminal, whenever you want to watch:
294
+
295
+ ```bash
296
+ npx -y @shibbirweb/mcp-db-read-only viewer --dir /Users/me/Library/Logs/mcp-db-read-only --port 4800
394
297
  ```
395
298
 
299
+ Then open **http://127.0.0.1:4800/**. You'll see every call: what was asked, the exact query sent, how long it took, and the result. It updates live, shows 20 per page (10, 20, 30 or 50 to choose from), and lets you filter and copy anything. Press Ctrl+C to close it.
300
+
301
+ > The viewer has no password. Anyone who can reach that port on your network can read the log while it runs.
302
+
303
+ Passwords are never written to the logs. More in the [Logging guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Logging-and-Viewer).
304
+
305
+ ---
306
+
307
+ ## Is it really read-only?
308
+
309
+ Yes, in two independent ways, so a mistake in one is caught by the other:
310
+
396
311
  ```text
397
- # Redis
398
- ACL SETUSER reader on >... ~* +@read -@dangerous
312
+ mcp-db-read-only --one file per call--> Log folder (DB_LOG_DIR) --> viewer command --live--> Your browser
313
+ (in your AI client) (in a terminal)
399
314
  ```
400
315
 
401
- Other limits worth knowing:
316
+ 1. **Before anything is sent**, every query is checked. Only reads are allowed: `SELECT` and friends for SQL, read commands for Redis, searches for Elasticsearch, and no `$out` or `$merge` for MongoDB.
317
+ 2. **The database is told to refuse writes too**, wherever it supports that: read-only sessions on MySQL, read-only transactions on PostgreSQL, a read-only file on SQLite, and so on.
318
+
319
+ **The best protection is still a read-only database account.** Then nothing can write through it, whatever happens. The [Databases guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Databases) shows how to create one for each database.
402
320
 
403
- - Results are truncated to 100 rows in the tool output. Add a `LIMIT` (or `$limit`) when reading large tables.
404
- - A column named exactly like a write keyword must be quoted where the validator scans for them: inside `WITH` queries, and in every SQL Server statement.
405
- - SQLite queries run in a separate process, so one that exceeds the timeout can be killed outright.
321
+ Keep in mind that the assistant **can read** whatever the account can see, and what it reads becomes part of your conversation with the AI provider. Only connect accounts that can see data you are happy to share. See [PRIVACY.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/PRIVACY.md).
406
322
 
407
323
  ---
408
324
 
409
- ## Known behaviour
325
+ ## Settings
410
326
 
411
- **Parallel tool calls.** The active connection is a single piece of process state. If a client issues several tool calls in one batch they are handled concurrently, so a `use_database` batched alongside a query is not guaranteed to land first. When a read must be pinned to a particular database, pass the per-call `database` argument instead.
327
+ All optional. Set them in the server's `env`.
412
328
 
413
- **Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. The live viewer, if running, closes its port with it. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
329
+ | Setting | What it does |
330
+ | --- | --- |
331
+ | `DB_URL` | The database to connect to at startup |
332
+ | `DB_PASSWORD` | The password for `DB_URL`, if you'd rather keep it out of the URL |
333
+ | `DB_PROFILES` | Several named databases, as JSON, to switch between |
334
+ | `DB_DEFAULT_PROFILE` | Which of those to start with |
335
+ | `DB_QUERY_TIMEOUT_MS` | Stop a query after this long (default 30000, that is 30 seconds) |
336
+ | `DB_CONNECT_TIMEOUT_MS` | Give up connecting after this long (default 10000) |
337
+ | `DB_LOG_DIR` | Save every call as a file in this folder |
338
+ | `DB_LOG=true` | Write every call to the client's log instead |
339
+ | `DB_LOG_FILE` | Write every call to one file instead |
340
+ | `DB_LOG_FORMAT` | `pretty` (default) or `json`, for `DB_LOG` and `DB_LOG_FILE` |
341
+ | `DB_LOG_PORT` | Run the viewer inside the server itself (the separate `viewer` command is usually better) |
342
+
343
+ Coming from `mcp-mysql-read-only`? Its `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` and `MYSQL_PROFILES` settings still work as they are.
344
+
345
+ Full details: [Configuration guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Configuration).
414
346
 
415
347
  ---
416
348
 
417
- ## Development and contributing
349
+ ## Troubleshooting
350
+
351
+ **"Can't connect" from Docker to a database on my computer.** Use `host.docker.internal` instead of `localhost`, and keep the `--add-host` line from the Docker example.
352
+
353
+ **The server won't start with npx.** Check `node --version` is 22.13 or newer.
354
+
355
+ **SQL Server says the certificate isn't trusted.** Add `?trustServerCertificate=true` to the URL for a local or development server.
418
356
 
419
- Building, testing against every engine in throwaway containers, the project structure and the contribution guide are in the [repository README](https://github.com/shibbirweb/mcp-db-read-only#development) and the [wiki](https://github.com/shibbirweb/mcp-db-read-only/wiki).
357
+ **"No database selected".** Your URL has no database name. Add one (`.../myapp`), or ask the assistant to list the databases and pick one.
420
358
 
421
- ## Changelog
359
+ **My password has `@` or `#` in it.** Use `DB_PASSWORD`, or the `{"url": ..., "password": ...}` form in `DB_PROFILES`.
422
360
 
423
- Release history is in [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md).
361
+ **The viewer says the port is in use.** Something else is using it. Close that, or pick another port with `--port 4801`.
362
+
363
+ More answers in the [Troubleshooting guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Troubleshooting).
364
+
365
+ ---
424
366
 
425
- ## Privacy
367
+ ## Learn more
426
368
 
427
- The server sends nothing anywhere except to the databases you point it at: no telemetry, no analytics, nothing written to disk, nothing kept after it exits. What does leave your machine is whatever your assistant reads, since query results become conversation content. [PRIVACY.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/PRIVACY.md) sets out both halves.
369
+ - [Wiki](https://github.com/shibbirweb/mcp-db-read-only/wiki): user guides for every feature, plus developer documentation
370
+ - [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md): what changed in each version
371
+ - [PRIVACY.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/PRIVACY.md): what the server sends where (nothing, except to your databases)
372
+ - Contributing: pull requests to `master` are welcome. `./scripts/test-in-docker.sh` runs the full test suite against every database in throwaway containers; see [Testing](https://github.com/shibbirweb/mcp-db-read-only/wiki/Testing). If you change this README, change [README.dockerhub.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/README.dockerhub.md) to match.
428
373
 
429
374
  ## License
430
375