@shibbirweb/mcp-db-read-only 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.dockerhub.md +354 -0
  4. package/README.md +387 -0
  5. package/dist/ApplicationFactory.js +144 -0
  6. package/dist/config/EnvironmentConfigLoader.js +179 -0
  7. package/dist/config/PackageVersionLoader.js +43 -0
  8. package/dist/connections/ConnectionManager.js +101 -0
  9. package/dist/connections/ConnectionRegistry.js +109 -0
  10. package/dist/connections/ConnectionTargetFactory.js +104 -0
  11. package/dist/connections/ConnectionUrlParser.js +195 -0
  12. package/dist/domain/ConnectionProfile.js +39 -0
  13. package/dist/domain/ConnectionTarget.js +139 -0
  14. package/dist/domain/Engine.js +159 -0
  15. package/dist/drivers/BaseDriver.js +35 -0
  16. package/dist/drivers/DatabaseDriver.js +1 -0
  17. package/dist/drivers/DriverCache.js +107 -0
  18. package/dist/drivers/DriverProvider.js +71 -0
  19. package/dist/drivers/DriverRegistry.js +24 -0
  20. package/dist/drivers/GlobPattern.js +41 -0
  21. package/dist/drivers/LazyResource.js +56 -0
  22. package/dist/drivers/document/MongoDriver.js +187 -0
  23. package/dist/drivers/document/MongoSchemaSampler.js +74 -0
  24. package/dist/drivers/document/MongoStageAllowlist.js +87 -0
  25. package/dist/drivers/keyvalue/RedisCommandFlagsGuard.js +69 -0
  26. package/dist/drivers/keyvalue/RedisDriver.js +224 -0
  27. package/dist/drivers/search/ElasticsearchDriver.js +159 -0
  28. package/dist/drivers/sql/ClickHouseDriver.js +156 -0
  29. package/dist/drivers/sql/MsSqlDriver.js +147 -0
  30. package/dist/drivers/sql/MySqlDriver.js +144 -0
  31. package/dist/drivers/sql/MySqlSessionInitializer.js +100 -0
  32. package/dist/drivers/sql/PostgresDriver.js +176 -0
  33. package/dist/drivers/sql/SqlIdentifier.js +36 -0
  34. package/dist/drivers/sql/SqliteDriver.js +202 -0
  35. package/dist/drivers/sql/SqliteProtocol.js +7 -0
  36. package/dist/drivers/sql/SqliteWorker.js +71 -0
  37. package/dist/errors/ApplicationError.js +15 -0
  38. package/dist/errors/EngineMismatchError.js +15 -0
  39. package/dist/errors/InvalidConnectionUrlError.js +14 -0
  40. package/dist/errors/InvalidProfileDefinitionError.js +15 -0
  41. package/dist/errors/NoActiveConnectionError.js +13 -0
  42. package/dist/errors/NoDatabaseSelectedError.js +13 -0
  43. package/dist/errors/ObjectNotFoundError.js +14 -0
  44. package/dist/errors/UnknownProfileError.js +16 -0
  45. package/dist/errors/UnsupportedOperationError.js +14 -0
  46. package/dist/errors/index.js +9 -0
  47. package/dist/formatting/JsonSerializer.js +49 -0
  48. package/dist/formatting/RowFormatter.js +43 -0
  49. package/dist/formatting/ToolResponse.js +25 -0
  50. package/dist/index.js +15 -0
  51. package/dist/server/McpDbServer.js +69 -0
  52. package/dist/tools/BaseTool.js +42 -0
  53. package/dist/tools/DatabaseScopedTool.js +61 -0
  54. package/dist/tools/QueryTools.js +13 -0
  55. package/dist/tools/browse/DescribeTableTool.js +37 -0
  56. package/dist/tools/browse/GetForeignKeysTool.js +36 -0
  57. package/dist/tools/browse/GetTableIndexesTool.js +30 -0
  58. package/dist/tools/browse/GetTableSampleTool.js +50 -0
  59. package/dist/tools/browse/ListTablesTool.js +51 -0
  60. package/dist/tools/connection/ConnectTool.js +67 -0
  61. package/dist/tools/connection/CurrentConnectionTool.js +36 -0
  62. package/dist/tools/connection/ListConnectionsTool.js +38 -0
  63. package/dist/tools/connection/ListDatabasesTool.js +41 -0
  64. package/dist/tools/connection/UseConnectionTool.js +57 -0
  65. package/dist/tools/connection/UseDatabaseTool.js +45 -0
  66. package/dist/tools/document/AggregateTool.js +44 -0
  67. package/dist/tools/document/CountDocumentsTool.js +32 -0
  68. package/dist/tools/document/DistinctValuesTool.js +36 -0
  69. package/dist/tools/document/DocumentTool.js +38 -0
  70. package/dist/tools/document/FindDocumentsTool.js +56 -0
  71. package/dist/tools/keyvalue/RedisCommandTool.js +40 -0
  72. package/dist/tools/search/SearchTool.js +54 -0
  73. package/dist/tools/sql/RunQueryTool.js +46 -0
  74. package/dist/types/config.types.js +1 -0
  75. package/dist/types/connection.types.js +1 -0
  76. package/dist/types/driver.types.js +1 -0
  77. package/dist/types/index.js +1 -0
  78. package/dist/types/tool.types.js +1 -0
  79. package/dist/types/validation.types.js +1 -0
  80. package/dist/validation/document/MongoOperatorGuard.js +72 -0
  81. package/dist/validation/keyvalue/RedisCommandValidator.js +176 -0
  82. package/dist/validation/names/NamePolicy.js +122 -0
  83. package/dist/validation/names/NamePolicyRegistry.js +33 -0
  84. package/dist/validation/search/SearchBodyValidator.js +56 -0
  85. package/dist/validation/sql/ReadOnlyQueryValidator.js +82 -0
  86. package/dist/validation/sql/SqlDialect.js +196 -0
  87. package/dist/validation/sql/SqlSkeletonizer.js +197 -0
  88. package/dist/validation/sql/SqlValidatorRegistry.js +24 -0
  89. package/dist/validation/sql/rules/AmbiguousSyntaxRule.js +23 -0
  90. package/dist/validation/sql/rules/EmptyQueryRule.js +16 -0
  91. package/dist/validation/sql/rules/ForbiddenPatternRule.js +30 -0
  92. package/dist/validation/sql/rules/LeadingKeywordRule.js +29 -0
  93. package/dist/validation/sql/rules/SingleStatementRule.js +26 -0
  94. package/dist/validation/sql/rules/SmuggledWriteRule.js +50 -0
  95. package/dist/validation/sql/rules/index.js +6 -0
  96. package/package.json +76 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/).
4
+
5
+ Each release is published to npm and Docker Hub from the same tag. Where a version reaches one channel but not the other, the entry says so.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0]
10
+
11
+ First release. Not yet published to npm, Docker Hub or the MCP Registry.
12
+
13
+ ### Added
14
+
15
+ - Read-only access to MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, ClickHouse, MongoDB, Redis, Elasticsearch and OpenSearch from one server, with the connection, database and engine switchable mid-conversation.
16
+ - Connections configured as URLs, through `DB_URL`, `DB_PROFILES` or the `connect` tool, with the password optionally given separately.
17
+ - The configuration of `mcp-mysql-read-only` (`MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PROFILES` and the rest) is read unchanged, so the image or package can be swapped in place.
18
+ - Browse tools that work on every engine: `list_tables` (with a glob `pattern`), `describe_table`, `get_table_indexes`, `get_foreign_keys`, `get_table_sample`.
19
+ - Query tools per engine family: `run_query` for SQL, `find_documents`, `aggregate`, `count_documents` and `distinct_values` for MongoDB, `search` for Elasticsearch and OpenSearch, `redis_command` for Redis.
20
+ - Two independent read-only layers on every engine, each proved by integration tests that send writes straight to the driver.
21
+ - A dialect-aware SQL validator that lexes PostgreSQL dollar quotes and `E''` strings, SQL Server brackets and ClickHouse heredocs, scans every T-SQL statement for writes, and refuses constructs it cannot read with certainty, such as nested comments and MySQL executable comments.
22
+ - SQLite runs in a separate process, so a query past the timeout is killed rather than freezing the server.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Md. Shibbir Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,354 @@
1
+ <!--
2
+ This file is the Docker Hub description, published by
3
+ .github/workflows/dockerhub-description.yml via `readme-filepath`.
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.
11
+ -->
12
+
13
+ # mcp-db-read-only
14
+
15
+ [![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
+ [![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
+ [![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
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shibbirweb/mcp-db-read-only/blob/master/LICENSE)
20
+
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` |
35
+
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.
37
+
38
+ Runs from npm with `npx`, or entirely in Docker with nothing installed on your machine.
39
+
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
+ ```
61
+
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
+ ---
65
+
66
+ ## Supported tags
67
+
68
+ `0.1.0`, `0.1`, `0`, `latest`, built for `linux/amd64` and `linux/arm64`.
69
+
70
+ ---
71
+
72
+ ## Quick start
73
+
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
+ ```
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`:
104
+
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
+ }
118
+ ```
119
+
120
+ Or the same server in Docker:
121
+
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
+ ```
137
+
138
+ ### Claude Code
139
+
140
+ The same shape, in `.mcp.json` at your project root. Either form above works.
141
+
142
+ Restart the client once. After that you never need to restart it to change database.
143
+
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).
145
+
146
+ ### Coming from mcp-mysql-read-only
147
+
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.
149
+
150
+ ---
151
+
152
+ ## Switching connections
153
+
154
+ Just ask. These map onto the connection tools:
155
+
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"
159
+
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 |
166
+
167
+ ```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 ------------|
181
+ ```
182
+
183
+ A switch that fails verification is never committed, so the previous connection stays active and the session keeps working.
184
+
185
+ ### Named profiles
186
+
187
+ Define several connections up front with `DB_PROFILES`, a JSON object whose values are URLs, or objects with a separate password:
188
+
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
+ }
198
+ ```
199
+
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.
201
+
202
+ ### Reaching somewhere not in the profiles
203
+
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.
205
+
206
+ ### URL details
207
+
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 |
218
+
219
+ ---
220
+
221
+ ## Tools
222
+
223
+ ### Connection
224
+
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` |
233
+
234
+ ### Browsing, on every engine
235
+
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 |
243
+
244
+ `list_tables` takes an optional glob `pattern`, such as `user*`, which is how you browse a Redis instance with millions of keys.
245
+
246
+ ### Querying, per engine family
247
+
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 |
257
+
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.
259
+
260
+ Every reading tool also accepts an optional `database`, applied to that call only, leaving the active connection alone.
261
+
262
+ ---
263
+
264
+ ## Configuration
265
+
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 |
275
+
276
+ None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
277
+
278
+ Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names a real profile, else `default`, else the first one defined.
279
+
280
+ ---
281
+
282
+ ## Security
283
+
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.
285
+
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 |
296
+
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.
298
+
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.
300
+
301
+ ### What this is not
302
+
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.
304
+
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:
306
+
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;
312
+ ```
313
+
314
+ ```js
315
+ // MongoDB
316
+ db.createUser({ user: "reader", pwd: "...", roles: [{ role: "read", db: "app" }] });
317
+ ```
318
+
319
+ ```text
320
+ # Redis
321
+ ACL SETUSER reader on >... ~* +@read -@dangerous
322
+ ```
323
+
324
+ Other limits worth knowing:
325
+
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.
329
+
330
+ ---
331
+
332
+ ## Known behaviour
333
+
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.
335
+
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.
337
+
338
+ ---
339
+
340
+ ## Development and contributing
341
+
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).
343
+
344
+ ## Changelog
345
+
346
+ Release history is in [CHANGELOG.md](https://github.com/shibbirweb/mcp-db-read-only/blob/master/CHANGELOG.md).
347
+
348
+ ## Privacy
349
+
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.
351
+
352
+ ## License
353
+
354
+ [MIT](https://github.com/shibbirweb/mcp-db-read-only/blob/master/LICENSE) © Md. Shibbir Ahmed