@shibbirweb/mcp-db-read-only 0.2.0 → 1.0.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,12 +2,8 @@
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
@@ -15,92 +11,32 @@
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)
17
13
  [![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)
19
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shibbirweb/mcp-db-read-only/blob/master/LICENSE)
20
15
 
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` |
16
+ Let your AI assistant **look at your databases without being able to change them.**
35
17
 
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.
18
+ 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
19
 
38
- Runs from npm with `npx`, or entirely in Docker with nothing installed on your machine.
20
+ - **Every popular database, one server.** Switch between them in the middle of a conversation.
21
+ - **Read-only, twice over.** Every query is checked before it is sent, and the database itself is also told to refuse writes.
22
+ - **No restart to switch.** Change database, server or engine by asking.
23
+ - **Optional logging**, with a live page in your browser that shows every query as it happens.
39
24
 
40
- ```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` )
60
- ```
25
+ Works with Claude Desktop, Claude Code, and any other [MCP](https://modelcontextprotocol.io) client.
61
26
 
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.
63
-
64
- ---
27
+ **Source and full documentation: [github.com/shibbirweb/mcp-db-read-only](https://github.com/shibbirweb/mcp-db-read-only)**
65
28
 
66
29
  ## Supported tags
67
30
 
68
- `0.2.0`, `0.2`, `0`, `latest`, built for `linux/amd64` and `linux/arm64`.
31
+ `1.0.0`, `1.0`, `1`, `latest`, built for `linux/amd64` and `linux/arm64`.
69
32
 
70
33
  ---
71
34
 
72
35
  ## Quick start
73
36
 
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
37
+ **1. Have Node.js 22.13 or newer** (`node --version`), or Docker.
83
38
 
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
- ```
90
-
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`:
39
+ **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
40
 
105
41
  ```json
106
42
  {
@@ -109,322 +45,308 @@ Add to `claude_desktop_config.json`:
109
45
  "command": "npx",
110
46
  "args": ["-y", "@shibbirweb/mcp-db-read-only"],
111
47
  "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"
48
+ "DB_URL": "postgres://readonly:secret@localhost:5432/myapp"
114
49
  }
115
50
  }
116
51
  }
117
52
  }
118
53
  ```
119
54
 
120
- Or the same server in Docker:
55
+ Replace the `DB_URL` with your own database. The examples below show one for every kind.
121
56
 
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
- ```
57
+ **3. Restart the client once, and ask away:**
58
+
59
+ > "What tables are in my database?"
60
+ > "Show me the 5 newest orders."
61
+ > "How many customers are in each country?"
137
62
 
138
- ### Claude Code
63
+ That's it. You never need to restart again to change database; just ask the assistant to switch.
139
64
 
140
- The same shape, in `.mcp.json` at your project root. Either form above works.
65
+ ---
141
66
 
142
- Restart the client once. After that you never need to restart it to change database.
67
+ ## Examples for each database
143
68
 
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).
69
+ Each database has a URL **format**, then a real **example** to copy and change. Put the finished URL in `DB_URL`.
145
70
 
146
- ### Coming from mcp-mysql-read-only
71
+ Replace each `[PLACEHOLDER]` with your own value:
147
72
 
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.
73
+ | Placeholder | What to put there |
74
+ | --- | --- |
75
+ | `[USER]` | The database user name |
76
+ | `[PASSWORD]` | That user's password |
77
+ | `[HOST]` | The server's address, e.g. `localhost` or `db.example.com` |
78
+ | `[PORT]` | The server's port. Optional: leave out `:[PORT]` to use the usual one shown for each database |
79
+ | `[DATABASE]` | The database name. Optional for most: leave it out and ask the assistant to list them |
149
80
 
150
- ---
81
+ No password? Leave out `:[PASSWORD]`. No user either? Leave out `[USER]:[PASSWORD]@` entirely.
151
82
 
152
- ## Switching connections
83
+ ### MySQL and MariaDB
153
84
 
154
- Just ask. These map onto the connection tools:
85
+ Format (usual port 3306):
155
86
 
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"
87
+ ```text
88
+ mysql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
89
+ mariadb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
90
+ ```
159
91
 
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 |
92
+ Example:
166
93
 
167
94
  ```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 ------------|
95
+ mysql://readonly:secret@localhost:3306/shop
181
96
  ```
182
97
 
183
- A switch that fails verification is never committed, so the previous connection stays active and the session keeps working.
98
+ > "List the tables in shop." · "Describe the orders table." · "What were last month's top 10 products by revenue?"
184
99
 
185
- ### Named profiles
100
+ ### PostgreSQL
186
101
 
187
- Define several connections up front with `DB_PROFILES`, a JSON object whose values are URLs, or objects with a separate password:
102
+ Format (usual port 5432). Add `?sslmode=require` to use TLS:
188
103
 
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
- }
104
+ ```text
105
+ postgres://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
198
106
  ```
199
107
 
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.
108
+ Example:
201
109
 
202
- ### Reaching somewhere not in the profiles
110
+ ```text
111
+ postgres://readonly:secret@localhost:5432/myapp
112
+ ```
203
113
 
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.
114
+ > "Which tables are in the reporting schema?" · "Show the foreign keys on invoices." · "Count signups per day this week."
205
115
 
206
- ### URL details
116
+ ### SQLite
207
117
 
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 |
118
+ Format (three slashes, then the full path to the file):
218
119
 
219
- ---
120
+ ```text
121
+ sqlite:///[PATH_TO_FILE]
122
+ ```
220
123
 
221
- ## Tools
124
+ Example:
222
125
 
223
- ### Connection
126
+ ```text
127
+ sqlite:///Users/me/data/app.db
128
+ ```
224
129
 
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` |
130
+ The file is opened read-only.
233
131
 
234
- ### Browsing, on every engine
132
+ > "What tables does this file have?" · "Show 10 rows from notes."
235
133
 
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 |
134
+ ### SQL Server (and Azure SQL)
243
135
 
244
- `list_tables` takes an optional glob `pattern`, such as `user*`, which is how you browse a Redis instance with millions of keys.
136
+ Format (usual port 1433). Add `?trustServerCertificate=true` for a local server with a self-signed certificate:
245
137
 
246
- ### Querying, per engine family
138
+ ```text
139
+ mssql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
140
+ ```
247
141
 
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 |
142
+ Example:
257
143
 
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.
144
+ ```text
145
+ mssql://readonly:secret@localhost:1433/Sales?trustServerCertificate=true
146
+ ```
259
147
 
260
- Every reading tool also accepts an optional `database`, applied to that call only, leaving the active connection alone.
148
+ > "List the tables in Sales." · "Show the top 5 customers by order total." (SQL Server uses `TOP 5`, not `LIMIT`; the assistant knows.)
261
149
 
262
- ---
150
+ ### ClickHouse
263
151
 
264
- ## Configuration
152
+ Format (usual port 8123, or 8443 with `clickhouse+https`):
265
153
 
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 |
154
+ ```text
155
+ clickhouse://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
156
+ clickhouse+https://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
157
+ ```
281
158
 
282
- None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
159
+ Example:
283
160
 
284
- Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names a real profile, else `default`, else the first one defined.
161
+ ```text
162
+ clickhouse://reader:secret@localhost:8123/analytics
163
+ ```
285
164
 
286
- ---
165
+ > "How many events per hour did we have yesterday?" · "What is the sorting key of the events table?"
287
166
 
288
- ## Call logging
167
+ ### MongoDB
289
168
 
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.
169
+ Format (usual port 27017). Use `mongodb+srv` for MongoDB Atlas, with no port:
291
170
 
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` |
171
+ ```text
172
+ mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]?authSource=admin
173
+ mongodb+srv://[USER]:[PASSWORD]@[CLUSTER_HOST]/[DATABASE]
174
+ ```
298
175
 
299
- A pretty entry:
176
+ Example:
300
177
 
301
178
  ```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
- └─
179
+ mongodb://reader:secret@localhost:27017/myapp?authSource=admin
318
180
  ```
319
181
 
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.
182
+ > "What collections are in myapp?" · "What fields do documents in users have?" · "Find the 5 most recent orders over 100." · "Count users by country."
326
183
 
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.
184
+ ### Redis (and Valkey, KeyDB)
328
185
 
329
- ### Live viewer in the browser
186
+ Format (usual port 6379). `[DB_NUMBER]` is the database number, 0 if left out; `rediss` means TLS:
330
187
 
331
- Add `DB_LOG_PORT` to watch calls arrive in a browser page, updating the moment each one finishes:
188
+ ```text
189
+ redis://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
190
+ rediss://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
191
+ ```
332
192
 
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/
193
+ Example:
194
+
195
+ ```text
196
+ redis://localhost:6379/0
336
197
  ```
337
198
 
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 |
199
+ > "Which keys start with session:?" · "What's inside user:42?" · "How long until cache:home expires?"
342
200
 
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.
201
+ ### Elasticsearch and OpenSearch
344
202
 
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.
203
+ Format (usual port 9200, no database). Add `+https` for TLS, or `?api_key=[API_KEY]` instead of a user and password:
346
204
 
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.
205
+ ```text
206
+ elasticsearch://[USER]:[PASSWORD]@[HOST]:[PORT]
207
+ opensearch://[USER]:[PASSWORD]@[HOST]:[PORT]
208
+ ```
348
209
 
349
- With no `DB_LOG_PORT`, nothing listens on any port.
210
+ Example:
350
211
 
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).
212
+ ```text
213
+ elasticsearch+https://elastic:secret@search.example.com:9200
214
+ ```
352
215
 
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.
216
+ > "What indices do we have?" · "Find error logs from the last hour." · "How many documents are in logs-2026.09?"
354
217
 
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.
218
+ 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).
356
219
 
357
220
  ---
358
221
 
359
- ## Security
222
+ ## Using several databases
360
223
 
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.
224
+ Give each one a name with `DB_PROFILES`, and switch by asking ("switch to legacy", "use the cache"):
362
225
 
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 |
226
+ ```json
227
+ "env": {
228
+ "DB_PROFILES": "{\"app\": \"postgres://readonly@localhost/app\", \"legacy\": \"mysql://readonly@localhost/shop\", \"cache\": \"redis://localhost:6379/0\"}",
229
+ "DB_DEFAULT_PROFILE": "app"
230
+ }
231
+ ```
373
232
 
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.
233
+ You can also connect to a database you didn't list, mid-conversation: *"connect to postgres://readonly@10.0.0.5/reports"*.
375
234
 
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.
235
+ **Password with special characters** (`@`, `/`, `#`)? Give it separately instead of inside the URL: `DB_PASSWORD` next to `DB_URL`, or `{"url": "...", "password": "..."}` inside `DB_PROFILES`.
377
236
 
378
- ### What this is not
237
+ ---
379
238
 
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.
239
+ ## Running with Docker
381
240
 
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:
241
+ Nothing to install but Docker:
383
242
 
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;
243
+ ```json
244
+ {
245
+ "mcpServers": {
246
+ "databases": {
247
+ "command": "docker",
248
+ "args": [
249
+ "run", "-i", "--rm",
250
+ "--add-host", "host.docker.internal:host-gateway",
251
+ "-e", "DB_URL=postgres://readonly:secret@host.docker.internal:5432/myapp",
252
+ "shibbirweb/mcp-db-read-only"
253
+ ]
254
+ }
255
+ }
256
+ }
389
257
  ```
390
258
 
391
- ```js
392
- // MongoDB
393
- db.createUser({ user: "reader", pwd: "...", roles: [{ role: "read", db: "app" }] });
259
+ 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`.
260
+
261
+ ---
262
+
263
+ ## Watching what the assistant does
264
+
265
+ Turn on logging to keep a record of every query the assistant runs, and see them live in your browser.
266
+
267
+ **1. Save logs to a folder** by adding this to the server's `env`:
268
+
269
+ ```json
270
+ "DB_LOG_DIR": "/Users/me/Library/Logs/mcp-db-read-only"
394
271
  ```
395
272
 
396
- ```text
397
- # Redis
398
- ACL SETUSER reader on >... ~* +@read -@dangerous
273
+ Each query is saved as its own file, one folder per day. Nothing is ever deleted automatically.
274
+
275
+ **2. Open the viewer** in a terminal, whenever you want to watch:
276
+
277
+ ```bash
278
+ npx -y @shibbirweb/mcp-db-read-only viewer --dir /Users/me/Library/Logs/mcp-db-read-only --port 4800
399
279
  ```
400
280
 
401
- Other limits worth knowing:
281
+ 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.
402
282
 
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.
283
+ > The viewer has no password. Anyone who can reach that port on your network can read the log while it runs.
284
+
285
+ Passwords are never written to the logs. More in the [Logging guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Logging-and-Viewer).
406
286
 
407
287
  ---
408
288
 
409
- ## Known behaviour
289
+ ## Is it really read-only?
290
+
291
+ Yes, in two independent ways, so a mistake in one is caught by the other:
410
292
 
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.
293
+ 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.
294
+ 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.
412
295
 
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.
296
+ **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.
297
+
298
+ 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).
414
299
 
415
300
  ---
416
301
 
417
- ## Development and contributing
302
+ ## Settings
303
+
304
+ All optional. Set them in the server's `env`.
305
+
306
+ | Setting | What it does |
307
+ | --- | --- |
308
+ | `DB_URL` | The database to connect to at startup |
309
+ | `DB_PASSWORD` | The password for `DB_URL`, if you'd rather keep it out of the URL |
310
+ | `DB_PROFILES` | Several named databases, as JSON, to switch between |
311
+ | `DB_DEFAULT_PROFILE` | Which of those to start with |
312
+ | `DB_QUERY_TIMEOUT_MS` | Stop a query after this long (default 30000, that is 30 seconds) |
313
+ | `DB_CONNECT_TIMEOUT_MS` | Give up connecting after this long (default 10000) |
314
+ | `DB_LOG_DIR` | Save every call as a file in this folder |
315
+ | `DB_LOG=true` | Write every call to the client's log instead |
316
+ | `DB_LOG_FILE` | Write every call to one file instead |
317
+ | `DB_LOG_FORMAT` | `pretty` (default) or `json`, for `DB_LOG` and `DB_LOG_FILE` |
318
+ | `DB_LOG_PORT` | Run the viewer inside the server itself (the separate `viewer` command is usually better) |
319
+
320
+ Coming from `mcp-mysql-read-only`? Its `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` and `MYSQL_PROFILES` settings still work as they are.
321
+
322
+ Full details: [Configuration guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Configuration).
323
+
324
+ ---
418
325
 
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).
326
+ ## Troubleshooting
420
327
 
421
- ## Changelog
328
+ **"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.
422
329
 
423
- Release history is in [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md).
330
+ **The server won't start with npx.** Check `node --version` is 22.13 or newer.
331
+
332
+ **SQL Server says the certificate isn't trusted.** Add `?trustServerCertificate=true` to the URL for a local or development server.
333
+
334
+ **"No database selected".** Your URL has no database name. Add one (`.../myapp`), or ask the assistant to list the databases and pick one.
335
+
336
+ **My password has `@` or `#` in it.** Use `DB_PASSWORD`, or the `{"url": ..., "password": ...}` form in `DB_PROFILES`.
337
+
338
+ **The viewer says the port is in use.** Something else is using it. Close that, or pick another port with `--port 4801`.
339
+
340
+ More answers in the [Troubleshooting guide](https://github.com/shibbirweb/mcp-db-read-only/wiki/Troubleshooting).
341
+
342
+ ---
424
343
 
425
- ## Privacy
344
+ ## Learn more
426
345
 
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.
346
+ - [Wiki](https://github.com/shibbirweb/mcp-db-read-only/wiki): user guides for every feature, plus developer documentation
347
+ - [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md): what changed in each version
348
+ - [PRIVACY.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/PRIVACY.md): what the server sends where (nothing, except to your databases)
349
+ - 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
350
 
429
351
  ## License
430
352