@shibbirweb/mcp-db-read-only 0.1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.dockerhub.md +233 -234
  3. package/README.md +221 -269
  4. package/dist/ApplicationFactory.js +111 -14
  5. package/dist/cli/ViewerCommand.js +117 -0
  6. package/dist/config/EnvironmentConfigLoader.js +58 -0
  7. package/dist/drivers/BaseDriver.js +10 -1
  8. package/dist/drivers/document/MongoDriver.js +27 -30
  9. package/dist/drivers/keyvalue/RedisDriver.js +20 -5
  10. package/dist/drivers/search/ElasticsearchDriver.js +13 -9
  11. package/dist/drivers/sql/ClickHouseDriver.js +12 -13
  12. package/dist/drivers/sql/MsSqlDriver.js +6 -6
  13. package/dist/drivers/sql/MySqlDriver.js +7 -5
  14. package/dist/drivers/sql/MySqlSessionInitializer.js +17 -2
  15. package/dist/drivers/sql/PostgresDriver.js +6 -6
  16. package/dist/drivers/sql/SqliteDriver.js +3 -3
  17. package/dist/formatting/JsonSerializer.js +3 -2
  18. package/dist/index.js +16 -7
  19. package/dist/logging/CallLogger.js +139 -0
  20. package/dist/logging/LogChannel.js +18 -0
  21. package/dist/logging/LogFormatter.js +80 -0
  22. package/dist/logging/LogRecords.js +7 -0
  23. package/dist/logging/LogSink.js +38 -0
  24. package/dist/logging/RecordJson.js +39 -0
  25. package/dist/logging/Redactor.js +95 -0
  26. package/dist/logging/StatementTracer.js +9 -0
  27. package/dist/logging/ToolCallObserver.js +6 -0
  28. package/dist/logging/store/FolderLogChannel.js +56 -0
  29. package/dist/logging/store/FolderLogStore.js +214 -0
  30. package/dist/logging/store/LogFileNames.js +57 -0
  31. package/dist/logging/store/LogStore.js +18 -0
  32. package/dist/logging/store/MemoryLogStore.js +70 -0
  33. package/dist/logging/viewer/LiveLogViewer.js +264 -0
  34. package/dist/logging/viewer/LiveViewerObserver.js +62 -0
  35. package/dist/logging/viewer/ViewerAssets.js +625 -0
  36. package/dist/server/BackgroundService.js +1 -0
  37. package/dist/server/McpDbServer.js +16 -2
  38. package/dist/tools/BaseTool.js +8 -2
  39. package/dist/tools/connection/CurrentConnectionTool.js +11 -2
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,9 +6,29 @@ Each release is published to npm and Docker Hub from the same tag. Where a versi
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0]
10
+
11
+ First stable release. Not yet published to npm, Docker Hub or the MCP Registry.
12
+
13
+ ### Added
14
+
15
+ - `mcp-db-read-only viewer --dir <folder> --port <port>`: the live log viewer as its own process, reading a `DB_LOG_DIR` folder, with no MCP server or database. Run it in a terminal so the servers an MCP client starts only write logs and never hold a port. It exits with a clear error when the port is taken.
16
+
17
+ ## [0.2.0]
18
+
19
+ Not yet published to npm, Docker Hub or the MCP Registry.
20
+
21
+ ### Added
22
+
23
+ - Optional call logging (`DB_LOG`, `DB_LOG_FILE`, `DB_LOG_FORMAT`): every tool call with its input, each statement sent to the database, and the full output, in a readable boxed format or as JSON lines. Credentials are always redacted, and a logging failure never fails a call.
24
+ - A permanent log folder (`DB_LOG_DIR`): every entry saved as its own pretty JSON file, in a folder per day, never deleted, shared safely by every copy of the server.
25
+ - A live log viewer in the browser (`DB_LOG_PORT`, `DB_LOG_HISTORY`): paginated, 20 per page by default with 10, 20, 30 or 50 to choose from; filters across everything logged; a copy icon on every block; and live updates as each call finishes. With a log folder it shows every copy's calls, across restarts. Off unless a port is set; it has no access control and listens on all interfaces.
26
+ - The viewer binds its port on the first tool call, so the copy of the server actually in use gets it. A busy port is explained once in the chat, naming the process holding it, and retried on every call.
27
+ - `current_connection` reports the viewer's state.
28
+
9
29
  ## [0.1.0]
10
30
 
11
- First release. Not yet published to npm, Docker Hub or the MCP Registry.
31
+ First version. Never published to npm, Docker Hub or the MCP Registry.
12
32
 
13
33
  ### Added
14
34
 
@@ -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,339 +11,342 @@
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
 
16
+ Let your AI assistant **look at your databases without being able to change them.**
17
+
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.
19
+
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.
24
+
25
+ Works with Claude Desktop, Claude Code, and any other [MCP](https://modelcontextprotocol.io) client.
26
+
21
27
  **Source and full documentation: [github.com/shibbirweb/mcp-db-read-only](https://github.com/shibbirweb/mcp-db-read-only)**
22
28
 
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**.
29
+ ## Supported tags
24
30
 
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` |
31
+ `1.0.0`, `1.0`, `1`, `latest`, built for `linux/amd64` and `linux/arm64`.
35
32
 
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.
33
+ ---
37
34
 
38
- Runs from npm with `npx`, or entirely in Docker with nothing installed on your machine.
35
+ ## Quick start
39
36
 
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` )
37
+ **1. Have Node.js 22.13 or newer** (`node --version`), or Docker.
38
+
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:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "databases": {
45
+ "command": "npx",
46
+ "args": ["-y", "@shibbirweb/mcp-db-read-only"],
47
+ "env": {
48
+ "DB_URL": "postgres://readonly:secret@localhost:5432/myapp"
49
+ }
50
+ }
51
+ }
52
+ }
60
53
  ```
61
54
 
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.
55
+ Replace the `DB_URL` with your own database. The examples below show one for every kind.
63
56
 
64
- ---
57
+ **3. Restart the client once, and ask away:**
65
58
 
66
- ## Supported tags
59
+ > "What tables are in my database?"
60
+ > "Show me the 5 newest orders."
61
+ > "How many customers are in each country?"
67
62
 
68
- `0.1.0`, `0.1`, `0`, `latest`, built for `linux/amd64` and `linux/arm64`.
63
+ That's it. You never need to restart again to change database; just ask the assistant to switch.
69
64
 
70
65
  ---
71
66
 
72
- ## Quick start
67
+ ## Examples for each database
73
68
 
74
- ### npm
69
+ Each database has a URL **format**, then a real **example** to copy and change. Put the finished URL in `DB_URL`.
75
70
 
76
- ```bash
77
- DB_URL='postgres://readonly:secret@127.0.0.1:5432/my_database' npx -y @shibbirweb/mcp-db-read-only
78
- ```
71
+ Replace each `[PLACEHOLDER]` with your own value:
72
+
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 |
79
80
 
80
- Requires Node 22.13 or newer. There is no container in the way, so `127.0.0.1` means what you expect.
81
+ No password? Leave out `:[PASSWORD]`. No user either? Leave out `[USER]:[PASSWORD]@` entirely.
81
82
 
82
- ### Docker
83
+ ### MySQL and MariaDB
83
84
 
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
85
+ Format (usual port 3306):
86
+
87
+ ```text
88
+ mysql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
89
+ mariadb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
89
90
  ```
90
91
 
91
- Use `host.docker.internal` to reach a database on the same machine as Docker. Inside the container, `localhost` means the container itself.
92
+ Example:
92
93
 
93
- For SQLite in Docker, mount the file's directory read-only and point at the path inside the container:
94
+ ```text
95
+ mysql://readonly:secret@localhost:3306/shop
96
+ ```
94
97
 
95
- ```bash
96
- docker run -i --rm -v "$PWD/data:/data:ro" -e DB_URL='sqlite:///data/app.db' shibbirweb/mcp-db-read-only
98
+ > "List the tables in shop." · "Describe the orders table." · "What were last month's top 10 products by revenue?"
99
+
100
+ ### PostgreSQL
101
+
102
+ Format (usual port 5432). Add `?sslmode=require` to use TLS:
103
+
104
+ ```text
105
+ postgres://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
97
106
  ```
98
107
 
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.
108
+ Example:
100
109
 
101
- ### Claude Desktop
110
+ ```text
111
+ postgres://readonly:secret@localhost:5432/myapp
112
+ ```
102
113
 
103
- Add to `claude_desktop_config.json`:
114
+ > "Which tables are in the reporting schema?" · "Show the foreign keys on invoices." · "Count signups per day this week."
104
115
 
105
- ```json
106
- {
107
- "mcpServers": {
108
- "databases": {
109
- "command": "npx",
110
- "args": ["-y", "@shibbirweb/mcp-db-read-only"],
111
- "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"
114
- }
115
- }
116
- }
117
- }
116
+ ### SQLite
117
+
118
+ Format (three slashes, then the full path to the file):
119
+
120
+ ```text
121
+ sqlite:///[PATH_TO_FILE]
118
122
  ```
119
123
 
120
- Or the same server in Docker:
124
+ Example:
121
125
 
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
- }
126
+ ```text
127
+ sqlite:///Users/me/data/app.db
136
128
  ```
137
129
 
138
- ### Claude Code
130
+ The file is opened read-only.
139
131
 
140
- The same shape, in `.mcp.json` at your project root. Either form above works.
132
+ > "What tables does this file have?" · "Show 10 rows from notes."
141
133
 
142
- Restart the client once. After that you never need to restart it to change database.
134
+ ### SQL Server (and Azure SQL)
143
135
 
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).
136
+ Format (usual port 1433). Add `?trustServerCertificate=true` for a local server with a self-signed certificate:
145
137
 
146
- ### Coming from mcp-mysql-read-only
138
+ ```text
139
+ mssql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
140
+ ```
147
141
 
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.
142
+ Example:
149
143
 
150
- ---
144
+ ```text
145
+ mssql://readonly:secret@localhost:1433/Sales?trustServerCertificate=true
146
+ ```
151
147
 
152
- ## Switching connections
148
+ > "List the tables in Sales." · "Show the top 5 customers by order total." (SQL Server uses `TOP 5`, not `LIMIT`; the assistant knows.)
153
149
 
154
- Just ask. These map onto the connection tools:
150
+ ### ClickHouse
155
151
 
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"
152
+ Format (usual port 8123, or 8443 with `clickhouse+https`):
159
153
 
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 |
154
+ ```text
155
+ clickhouse://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
156
+ clickhouse+https://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
157
+ ```
158
+
159
+ Example:
166
160
 
167
161
  ```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 ------------|
162
+ clickhouse://reader:secret@localhost:8123/analytics
181
163
  ```
182
164
 
183
- A switch that fails verification is never committed, so the previous connection stays active and the session keeps working.
165
+ > "How many events per hour did we have yesterday?" · "What is the sorting key of the events table?"
184
166
 
185
- ### Named profiles
167
+ ### MongoDB
186
168
 
187
- Define several connections up front with `DB_PROFILES`, a JSON object whose values are URLs, or objects with a separate password:
169
+ Format (usual port 27017). Use `mongodb+srv` for MongoDB Atlas, with no port:
188
170
 
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
- }
171
+ ```text
172
+ mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]?authSource=admin
173
+ mongodb+srv://[USER]:[PASSWORD]@[CLUSTER_HOST]/[DATABASE]
198
174
  ```
199
175
 
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.
176
+ Example:
201
177
 
202
- ### Reaching somewhere not in the profiles
178
+ ```text
179
+ mongodb://reader:secret@localhost:27017/myapp?authSource=admin
180
+ ```
203
181
 
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.
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."
205
183
 
206
- ### URL details
184
+ ### Redis (and Valkey, KeyDB)
207
185
 
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 |
186
+ Format (usual port 6379). `[DB_NUMBER]` is the database number, 0 if left out; `rediss` means TLS:
218
187
 
219
- ---
188
+ ```text
189
+ redis://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
190
+ rediss://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
191
+ ```
220
192
 
221
- ## Tools
193
+ Example:
222
194
 
223
- ### Connection
195
+ ```text
196
+ redis://localhost:6379/0
197
+ ```
224
198
 
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` |
199
+ > "Which keys start with session:?" · "What's inside user:42?" · "How long until cache:home expires?"
233
200
 
234
- ### Browsing, on every engine
201
+ ### Elasticsearch and OpenSearch
235
202
 
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 |
203
+ Format (usual port 9200, no database). Add `+https` for TLS, or `?api_key=[API_KEY]` instead of a user and password:
243
204
 
244
- `list_tables` takes an optional glob `pattern`, such as `user*`, which is how you browse a Redis instance with millions of keys.
205
+ ```text
206
+ elasticsearch://[USER]:[PASSWORD]@[HOST]:[PORT]
207
+ opensearch://[USER]:[PASSWORD]@[HOST]:[PORT]
208
+ ```
245
209
 
246
- ### Querying, per engine family
210
+ Example:
247
211
 
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 |
212
+ ```text
213
+ elasticsearch+https://elastic:secret@search.example.com:9200
214
+ ```
257
215
 
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.
216
+ > "What indices do we have?" · "Find error logs from the last hour." · "How many documents are in logs-2026.09?"
259
217
 
260
- Every reading tool also accepts an optional `database`, applied to that call only, leaving the active connection alone.
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).
261
219
 
262
220
  ---
263
221
 
264
- ## Configuration
222
+ ## Using several databases
265
223
 
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
- | `MYSQL_*` | | The legacy MySQL-only variables, read unchanged. See above |
224
+ Give each one a name with `DB_PROFILES`, and switch by asking ("switch to legacy", "use the cache"):
225
+
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
+ ```
275
232
 
276
- None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
233
+ You can also connect to a database you didn't list, mid-conversation: *"connect to postgres://readonly@10.0.0.5/reports"*.
277
234
 
278
- Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names a real profile, else `default`, else the first one defined.
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`.
279
236
 
280
237
  ---
281
238
 
282
- ## Security
239
+ ## Running with Docker
283
240
 
284
- 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.
241
+ Nothing to install but Docker:
285
242
 
286
- | Engine | Layer one, in this server | Layer two |
287
- | --- | --- | --- |
288
- | MySQL, MariaDB | SQL validator | `SET SESSION TRANSACTION READ ONLY` on every connection; the driver cannot send a second statement |
289
- | 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 |
290
- | SQLite | SQL validator | The file is opened read-only by SQLite; extensions disabled |
291
- | SQL Server | SQL validator, scanning every statement since T-SQL needs no separators | Every batch runs in a transaction that is always rolled back |
292
- | ClickHouse | SQL validator, refusing table functions that reach outside the server | ClickHouse's own `readonly` setting on every query |
293
- | MongoDB | Operator denylist: `$out`, `$merge`, `$function`, `$where` anywhere | Stage allowlist in the driver, which only ever calls read operations |
294
- | Redis | Command allowlist | The server's own `COMMAND INFO` flags: a command is sent only if Redis itself calls it read-only |
295
- | Elasticsearch | Search body allowlist; index names cannot address an API | The driver can only reach fixed read endpoints |
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
+ }
257
+ ```
296
258
 
297
- 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.
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`.
298
260
 
299
- Integration tests prove layer two separately: they send writes straight to each driver, bypassing every validator, and assert the server refused or undid them.
261
+ ---
300
262
 
301
- ### What this is not
263
+ ## Watching what the assistant does
302
264
 
303
- **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.
265
+ Turn on logging to keep a record of every query the assistant runs, and see them live in your browser.
304
266
 
305
- **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:
267
+ **1. Save logs to a folder** by adding this to the server's `env`:
306
268
 
307
- ```sql
308
- -- MySQL
309
- CREATE USER 'readonly'@'%' IDENTIFIED BY '...'; GRANT SELECT ON app.* TO 'readonly'@'%';
310
- -- PostgreSQL
311
- CREATE ROLE readonly LOGIN PASSWORD '...'; GRANT pg_read_all_data TO readonly;
269
+ ```json
270
+ "DB_LOG_DIR": "/Users/me/Library/Logs/mcp-db-read-only"
312
271
  ```
313
272
 
314
- ```js
315
- // MongoDB
316
- db.createUser({ user: "reader", pwd: "...", roles: [{ role: "read", db: "app" }] });
317
- ```
273
+ Each query is saved as its own file, one folder per day. Nothing is ever deleted automatically.
318
274
 
319
- ```text
320
- # Redis
321
- ACL SETUSER reader on >... ~* +@read -@dangerous
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
322
279
  ```
323
280
 
324
- 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.
282
+
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).
286
+
287
+ ---
288
+
289
+ ## Is it really read-only?
290
+
291
+ Yes, in two independent ways, so a mistake in one is caught by the other:
292
+
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.
325
295
 
326
- - Results are truncated to 100 rows in the tool output. Add a `LIMIT` (or `$limit`) when reading large tables.
327
- - 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.
328
- - SQLite queries run in a separate process, so one that exceeds the timeout can be killed outright.
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).
329
299
 
330
300
  ---
331
301
 
332
- ## Known behaviour
302
+ ## Settings
333
303
 
334
- **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.
304
+ All optional. Set them in the server's `env`.
335
305
 
336
- **Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
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).
337
323
 
338
324
  ---
339
325
 
340
- ## Development and contributing
326
+ ## Troubleshooting
327
+
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.
329
+
330
+ **The server won't start with npx.** Check `node --version` is 22.13 or newer.
341
331
 
342
- 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).
332
+ **SQL Server says the certificate isn't trusted.** Add `?trustServerCertificate=true` to the URL for a local or development server.
343
333
 
344
- ## Changelog
334
+ **"No database selected".** Your URL has no database name. Add one (`.../myapp`), or ask the assistant to list the databases and pick one.
345
335
 
346
- Release history is in [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md).
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
+ ---
347
343
 
348
- ## Privacy
344
+ ## Learn more
349
345
 
350
- 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.
351
350
 
352
351
  ## License
353
352