dsh-db-mcp 0.6.9 → 0.7.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.
package/README.md CHANGED
@@ -1,370 +1,92 @@
1
1
  # dsh-db-mcp
2
2
 
3
3
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
4
+ [![npm](https://img.shields.io/npm/v/dsh-db-mcp.svg)](https://www.npmjs.com/package/dsh-db-mcp)
4
5
 
5
6
  English | [中文](README.zh.md)
6
7
 
7
- DSH plugin that adds a **Database** entry to the sidebar with a data-source
8
- management panel, backed by a Python MCP server that lets the agent query those
9
- databases through tools (not through the page).
8
+ A [DSH](https://github.com/deepseek-ai/deepseek-harness) plugin that hands your
9
+ databases to the agent. It adds a **Database** panel to the sidebar and publishes
10
+ every registered source as `mcp__db__*` tools.
10
11
 
11
- - One panel for every engine: SQLite, PostgreSQL, MySQL, Oracle.
12
- - One connection string fills the whole form; passwords never leave the machine
13
- and never come back to the page.
14
- - Import the connections DBeaver already has, including its encrypted passwords.
15
- - Read-only by default, with two independent write gates and an audit log.
16
- - The agent gets eight MCP tools, not a SQL editor in the UI.
12
+ **Zero configuration.** There is nothing to edit in DSH and no connection to type
13
+ twice: import the connections DBeaver already knows, then just ask.
17
14
 
18
- Requirements and staged plan: [`docs/requirements.md`](docs/requirements.md).
15
+ ## Zero configuration, in three senses
19
16
 
20
- ## What exists today (P7)
17
+ - **No DSH files.** The bundle patch inserts one Loader row. The plugin starts its
18
+ own Python MCP server over stdio and registers the tools on the host `tools`
19
+ service — no `dsh-mcp-client` row, no JSON to hand-edit, no env var to export.
20
+ - **No data source to fill in.** The importer reads `data-sources.json` from the
21
+ DBeaver workspace and decrypts `credentials-config.json` with DBeaver's own key,
22
+ so host, port, database, user and saved password arrive together. Ticking a box
23
+ is the whole setup step.
24
+ - **No Python to prepare.** The panel finds an interpreter, creates the venv and
25
+ installs the four driver packages from **Runtime → Install / repair
26
+ dependencies**.
21
27
 
22
- - **Host half** (`index.js` → `src/host.js`): a Cordis plugin serving
23
- `/plugins/db-mcp/api` from the DSH web server.
24
-
25
- | Route | Purpose |
26
- | ----- | ------- |
27
- | `GET /health` | plugin version, Host facts, Python discovery, runtime paths, registry state, MCP status, settings |
28
- | `GET /datasources` | registry, credentials projected away |
29
- | `GET /schema?datasource=…&table=…[&schema=…]` | read-only structure: tables, or the columns of one table |
30
- | `GET /groups` | declared groups plus every group label in use |
31
- | `GET /dbeaver/scan` | connections already configured in DBeaver |
32
- | `GET /mcp/status` | Python server phase, tool list, last error, stderr tail |
33
- | `GET /settings` · `POST /settings` | global write switch and audit switch |
34
- | `GET /audit?limit=N` | the tail of the tool-call audit log |
35
- | `GET /python/env` | interpreter, venv path, installed driver versions |
36
- | `POST /python/setup` | create the venv and `pip install -r server/requirements.txt` |
37
- | `POST /datasources/save` | create or update a source (`id` is immutable) |
38
- | `POST /datasources/delete` | remove a source and its stored secret |
39
- | `POST /datasources/test` | real login probe: SQLite over `node:sqlite`, the rest through the Python drivers |
40
- | `POST /datasources/bulk` | test / read-only / group / delete a set of sources |
41
- | `POST /groups/save` · `POST /groups/delete` | create, rename or delete a group |
42
- | `POST /dbeaver/import` | copy selected DBeaver connections into the registry |
43
- | `POST /mcp/start` · `POST /mcp/restart` | start or restart the Python MCP server |
44
- | `POST /mcp/call` | dev helper: call one MCP tool through the panel |
45
- | `POST /reload` | dev helper: re-import `src/host.js` without restarting DSH |
46
-
47
- - **Client half** (`client.js`): sidebar entry (`db-mcp`, order 25) plus the main
48
- panel with the source list, the minimal create/edit form (one connection string),
49
- test/delete/default actions, a read-only *structure* browser, the DBeaver import
50
- flow, a *Restart MCP* button, the runtime diagnostics card (tool count, global
51
- write switch, venv and package versions with a one-click install) and an
52
- *audit log* card. Copy is localized (zh/en) through the client `locale` service.
53
- - **Python server** (`server/`): `main.py` (FastMCP, eight tools), `engines.py`
54
- (per-engine connect/introspect/query), `store.py` (reads the registry).
55
- The plugin spawns it over stdio and publishes its tools as `mcp__db__*` on the
56
- host `tools` service — it does not go through a `dsh-mcp-client` Loader row.
57
-
58
- ### Tools the agent gets
59
-
60
- | Tool | Purpose |
61
- | ---- | ------- |
62
- | `mcp__db__list_datasources` | the registered sources (id, kind, target, read-only) |
63
- | `mcp__db__list_tables` | tables/views of one source |
64
- | `mcp__db__describe_table` | columns, types, nullability, primary key |
65
- | `mcp__db__sample_rows` | a few rows of one table |
66
- | `mcp__db__run_query` | one statement; read-only sources refuse writes |
67
- | `mcp__db__explain_query` | query plan |
68
- | `mcp__db__get_ddl` | `CREATE TABLE` statement (SQLite: as created; others: rebuilt from the catalog) |
69
- | `mcp__db__test_connection` | log in for real, run `select 1`, report server/user/schema |
70
-
71
- `datasource` is optional everywhere: omitted, the default source is used.
72
-
73
- ### Python environment
74
-
75
- Interpreter discovery order: plugin config `pythonPath` → `<DSH_HOME>/db-mcp/.venv/Scripts/python.exe` → that venv on POSIX → `python` → `python3`. Dependencies live in the runtime venv, not in the package:
76
-
77
- python -m venv C:\Users\<you>\.dsh\db-mcp\.venv
78
- C:\Users\<you>\.dsh\db-mcp\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
79
-
80
- `mcp` must stay on 1.x (2.x renamed `FastMCP` to `MCPServer`).
81
-
82
- ## Does the Test button really log in?
83
-
84
- Yes. For PostgreSQL, MySQL and Oracle the panel's **Test** action calls
85
- `mcp__db__test_connection` over the Python MCP server, so the probe is a real
86
- authenticated login plus `select 1` (Oracle uses `select 1 from dual`). The
87
- panel row then shows the server banner, the connected user and database, and how
88
- long the login took, for example:
89
-
90
- ```
91
- Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok
92
- PostgreSQL 15.19 · demo@demo · select version() ok
93
- MySQL 8.0.46 · demo@%@demo · select version() ok
94
- ```
95
-
96
- A wrong password or an unreachable listener now surfaces the driver's own
97
- message (`ORA-01017`, `DPY-6005`, `password authentication failed` — instead of
98
- "port reachable"). Testing an unsaved form works too: `test_connection` accepts
99
- `kind` + `config` + `password` without writing the registry first. When the
100
- Python server cannot start, the probe degrades to a TCP check and says so.
101
-
102
- ## Filling the form from one string
103
-
104
- The create/edit form is deliberately as small as DBeaver's connection dialog:
105
- **connection string**, **user**, **password** (with a *save password* box), and
106
- that is it. Pasting a JDBC URL, a DSN, an Oracle TNS descriptor or a `.db` path
107
- fills type, host, port, database/service, name and identifier; the derived values
108
- only show up under **More settings**, together with schema, SID, SSL mode,
109
- charset and the read-only switch.
110
-
111
- Accepted shapes (see `dev/parse.test.mjs` for the full matrix):
112
-
113
- ```
114
- postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
115
- jdbc:postgresql://localhost:5432/appdb
116
- mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
117
- jdbc:mariadb://10.0.0.5:3307/legacy
118
- oracle://system:oracle@localhost:1521/XEPDB1
119
- jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # service name
120
- jdbc:oracle:thin:@ora:1521:ORCL # SID
121
- (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
122
- jdbc:sqlite:C:\data\app.db
123
- sqlite:///C:/data/chinook.db
124
- C:\data\app.db / /var/lib/app.sqlite3
125
- ```
126
-
127
- A password found inside the URL is sent once to `POST /datasources/save` and
128
- then only stored in `secrets.json`; it is never echoed back.
129
-
130
- Writes are `POST` only (the same-origin check compares `Origin` with `Host`).
131
- Passwords never enter `datasources.json`; they live in `secrets.json` and are
132
- never returned to the page (`hasSecret` is the only signal).
133
-
134
- ## DBeaver import
135
-
136
- `GET /dbeaver/scan` looks for `data-sources.json` under
137
- `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\` (plus the `General` and
138
- Linux/macOS layouts). When the sibling `credentials-config.json` exists it is
139
- decrypted with DBeaver's built-in key (AES-128-CBC, zero IV, 16-byte prefix — see `Tatsh/dbeaver-creds`). A workspace that never saved passwords simply
140
- imports the connection with an empty password, which the UI shows as
141
- `no password`.
142
-
143
- Connections created through the DBeaver driver dialog carry only a JDBC URL
144
- (`configurationType: URL`) — no `database` field at all and no `user`. The
145
- importer therefore parses `jdbc:mysql://host:port/db`,
146
- `jdbc:postgresql://…` and `jdbc:oracle:thin:@…` for whatever the form fields
147
- omit, and takes the user from the decrypted credentials when it is stored
148
- there. The imported source is named after the DBeaver connection name (slugged
149
- into an id), keeps `readOnly: true`, and gets the saved password copied into
150
- `secrets.json`. Anything still missing is reported in `warnings`.
151
-
152
- A connection that is already configured is not offered twice: every source
153
- is identified by kind + host + port + database/service/SID + user (SQLite by
154
- file path), and a DBeaver entry that matches one of them comes back with
155
- `alreadyImported: true`, `duplicateId` and `duplicateName`. The panel disables
156
- its checkbox and says *already added as …*; `POST /dbeaver/import` reports such
157
- ids under `skipped` instead of creating a second copy.
158
-
159
- The panel navigates with a tab bar (*Data source list / New data source /
160
- Import from DBeaver / Runtime / Audit log*): the active step is plain text, every
161
- earlier step is a link back, and no page needs a separate "back to list" button.
162
-
163
- ## Test databases in Docker
164
-
165
- Nothing has to be installed on the host; every engine below is a throwaway
166
- container with a demo account (`demo` / `dbmcp-demo`).
28
+ ## Run
167
29
 
30
+ ```sh
31
+ dsh plugin --profile desktop add dsh-db-mcp # desktop app
32
+ dsh plugin --profile web add dsh-db-mcp # web UI
168
33
  ```
169
- # PostgreSQL — smallest and fastest to start, the recommended first target
170
- docker run -d --name dbmcp-pg -p 5432:5432 \
171
- -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
172
- postgres:15-alpine
173
-
174
- # MySQL
175
- docker run -d --name dbmcp-mysql -p 3306:3306 \
176
- -e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
177
- -e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
178
34
 
179
- # Oracle Free — several GB, a minute or two to become healthy
180
- docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
181
- gvenzl/oracle-free:23-slim
182
- ```
35
+ `--profile` picks the profile to install into (`desktop` and `web` ship with DSH;
36
+ any other name is created on first use). Remove with
37
+ `dsh plugin --profile desktop remove dsh-db-mcp`.
183
38
 
184
- On Windows, 1521 often falls inside a Hyper-V reserved port block and `docker run`
185
- fails with *"An attempt was made to access a socket in a way forbidden by its
186
- access permissions"*. Check `netsh interface ipv4 show excludedportrange
187
- protocol=tcp` and publish another host port instead, e.g. `-p 11521:1521`; the
188
- datasource then points at port 11521.
39
+ Then:
189
40
 
190
- Then create a source in the panel with the matching connection string, e.g.
191
- `postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo`,
192
- `mysql://demo:dbmcp-demo@127.0.0.1:3306/demo` or
193
- `oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1`.
41
+ 1. Sidebar → **Database**.
42
+ 2. **Import from DBeaver** → tick the connections → **Import selected**.
43
+ 3. Ask the agent something like *"which tables does my Chinook database have?"*
194
44
 
195
- ## Install into a profile
45
+ A connection whose password was never saved shows **no password**; open the source
46
+ once and type it — it goes to `secrets.json` and never comes back to the page.
196
47
 
197
- ```
198
- plugin_manager install_bundle target=<absolute path to this directory>
199
- ```
48
+ Prefer to start from scratch? **New data source** takes one connection string
49
+ (`postgresql://user:pw@host:5432/db`, a JDBC URL, an Oracle TNS descriptor or a
50
+ `.db` path) and fills the form from it.
200
51
 
201
- or, from npm:
52
+ ## What the agent gets
202
53
 
203
- ```
204
- dsh plugin add dsh-db-mcp@latest
205
- ```
206
-
207
- The bundle patch (`cordis.patch.yml`) inserts one Loader row with id `db-mcp`.
208
- A profile that links this directory keeps file edits live; the Host module is
209
- imported once per process, hence `POST /plugins/db-mcp/api/reload` (or an app
210
- restart) after changing `src/host.js`.
211
-
212
- ## Panel layout
213
-
214
- The page follows the same chrome as the other first-party panels:
215
-
216
- - a masthead with **‹ Back to chat** (closes the panel through
217
- `ctx.layout.selectPanel(null)`), the **Database** title, and the
218
- connection/version pills;
219
- - a tab row — **Data source list / New data source / Import from DBeaver /
220
- Runtime / Audit log** — with the active tab underlined.
221
-
222
- Picking **New data source** or **Import from DBeaver** swaps only the card below
223
- the tabs — the masthead and the tabs stay put. Editing reuses the **New data
224
- source** tab; the structure browser is a sub-view of **Data source list** with
225
- its own `← Sources` link. There is no second action bar: **Refresh** and
226
- **Restart MCP** sit in the source-list card header, and the list itself owns the
227
- group tools.
228
-
229
- ## Schema overview
230
-
231
- The **Structure** button on a source row opens a read-only browser: the tables
232
- (or views) of that source on the left, and the columns of the selected table on
233
- the right — name, type, nullability, default value and primary-key marker. It is
234
- served by `GET /schema`, which only forwards `list_tables` / `describe_table` to
235
- the MCP server, so the page itself can never run SQL. Nothing is cached: each
236
- open reads the live catalog.
237
-
238
- ## Groups and bulk actions
239
-
240
- Every source carries an optional **group** label (`group` in the registry,
241
- editable under *More settings*). Groups are user-owned: type a name in the
242
- **New group** box to create one, **double-click a group name to rename it** (or
243
- the delete button to drop the group), and drag a source row onto a group to move
244
- it there (drag it onto **Ungrouped** to clear the label). Double-clicking a
245
- source name renames just that source — its id never changes. The list renders one
246
- section per group, sorted by name with the ungrouped sources last; while no group
247
- exists the list keeps its flat look.
248
-
249
- Each group header carries a select-all box, so sources inside a group take part
250
- in bulk work exactly like loose ones. Tick any row to open the bulk bar: test all
251
- selected sources, flip them to read-only or writable, move them into a group, or
252
- delete them (two-step confirmation). Bulk moves go through
253
- `POST /datasources/bulk` with
254
- `{ action: 'test' | 'readOnly' | 'group' | 'delete', ids: [...], … }`, while group
255
- CRUD uses `GET /groups`, `POST /groups/save` and `POST /groups/delete`.
256
-
257
- ## Releasing
258
-
259
- The package is published from GitHub Actions with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers):
260
- `.github/workflows/publish.yml` trades the workflow's OIDC token for a short-lived
261
- publish credential, so no `NPM_TOKEN` secret exists anywhere.
262
-
263
- ```bash
264
- node dev/bump.mjs 0.6.5 # package.json + src/host.js
265
- git commit -am "chore: release 0.6.5"
266
- git tag v0.6.5 && git push origin main --tags
267
- ```
268
-
269
- The workflow refuses to publish when the tag and `package.json` disagree, runs the
270
- portable suites (`npm run test:ci`), publishes with `--provenance`, then waits for the
271
- version to appear in the registry. Local `npm publish` still works for the very first
272
- release, before a trusted publisher exists for the package.
273
-
274
- ## Development
275
-
276
- ```
277
- node dev/engines.test.mjs # 11 checks: engines.py commit/rollback + affectedRows, stub connection
278
- node dev/selftest.mjs # mounts src/host.js on a throwaway port, 46 checks
279
- node dev/shims.test.mjs # mounts index.js, checks routes, /reload, tool publication
280
- node dev/parse.test.mjs # 24 checks: connection-string parser (DSN/JDBC/TNS/paths)
281
- node dev/render.test.mjs # 40 checks: components and the result renderer, stub React
282
- node dev/i18n.test.mjs # 12 checks: en/zh parity, no dead keys, no stray copy
283
- node dev/p5.test.mjs # 15 checks: settings, audit log, python environment
284
- node dev/mcp.test.mjs # 19 checks: real Python server over stdio, real SQLite + Oracle logins
285
- ```
286
-
287
- `npm test` runs everything except `mcp.test.mjs`, which needs the venv and the
288
- Oracle container. `dev/engines.test.mjs` needs a Python interpreter (the plugin
289
- venv, else `python` on PATH) but no database: it drives `server/engines.py`
290
- against a stub DB-API connection, covering the commit/rollback and
291
- `affectedRows` contract of `run_query`. `node dev/bump.mjs 0.6.6` updates both
292
- version strings.
293
-
294
- ## Safety switches
295
-
296
- Two independent gates decide whether a statement may write:
297
-
298
- 1. **Global switch** — `allowWrite` in `<runtime>/settings.json`, toggled from the
299
- diagnostics card. Off by default; while off every write statement is refused
300
- with `user_error` and a hint pointing at the switch.
301
- 2. **Per-source flag** — `readOnly` on the data source, on by default. A source
302
- stays read-only even when the global switch is on until you clear its flag.
303
- Click the *read-only* / *writable* pill on a source row to flip it in one
304
- click; the pill turns amber while that source accepts writes, and the notice
305
- reminds you when the global switch is still off.
306
-
307
- The Python server reads the global flag from `DB_MCP_ALLOW_WRITE` at start-up; the
308
- host restarts it whenever the switch flips. Read-only is also enforced by the
309
- driver itself where the engine allows it (`mode=ro` for SQLite,
310
- `default_transaction_read_only` for PostgreSQL, `SET SESSION TRANSACTION READ ONLY`
311
- for MySQL).
312
-
313
- ### What the plugin runs, and with what
314
-
315
- - The Python server is started with a fixed interpreter path, a fixed script and a
316
- fixed argument list — never through a shell, so no string is ever re-parsed by
317
- `cmd.exe` or `/bin/sh`.
318
- - The child does **not** inherit the host environment. It gets a short allowlist
319
- (`PATH`, `TEMP`, `HOME`, `PYTHON*`, and similar) plus the three `DB_MCP_*` flags
320
- the plugin sets. Tokens or cloud credentials in the DSH process are not passed
321
- down.
322
- - Connections to PostgreSQL, MySQL and Oracle are opened by that child; only SQLite
323
- is read in-process. The raw TCP probe is a fallback used when the child cannot
324
- start, and it never sends credentials.
325
- - Passwords live in `secrets.json` (mode `0600`) and are never returned by any HTTP
326
- route; the panel only learns `hasSecret`.
327
-
328
- ## Audit log
329
-
330
- Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
331
- time, tool, source, duration, row count, the SQL truncated to 2000 characters and
332
- the error when it failed. Result rows are never logged. The file rotates to
333
- `audit.1.jsonl` at 512 KiB and logging can be turned off with `auditLog: false` in
334
- `settings.json`. The panel shows the last dozen entries on **your own clock** —
335
- the file keeps UTC so it stays comparable across machines, the card converts each
336
- stamp to the viewer's time zone and prints the offset it used.
337
-
338
- `dev/shims.test.mjs` also asserts that `package.json` has no UTF-8 BOM: the host
339
- parses it with `JSON.parse`, which rejects a BOM, and the plugin then shows up as
340
- `availability: missing` instead of loading. Editors that "add BOM" will break the
341
- bundle silently — if the sidebar entry disappears after a restart, check this
342
- first.
343
-
344
- ## Stage map
345
-
346
- | Stage | Content |
347
- | ----- | ------- |
348
- | P1 | installable skeleton: sidebar entry, empty panel, Host health API (done) |
349
- | P2 | data-source CRUD, connection test, DBeaver import (done) |
350
- | P3 | Python MCP server, tools published as `mcp__db__*` (done) |
351
- | P4 | PostgreSQL / MySQL / Oracle dialects (done) |
352
- | P5 | diagnostics, one-click venv, reload, audit log, global write switch (done) |
353
- | P6 | i18n polish and docs (done) |
354
- | P7 | read-only schema overview, groups, bulk actions (done) |
54
+ | Tool | Purpose |
55
+ | ---- | ------- |
56
+ | `mcp__db__list_datasources` | registered sources: id, kind, target, read-only |
57
+ | `mcp__db__list_tables` | tables and views of a source |
58
+ | `mcp__db__describe_table` | columns, types, nullability, primary key |
59
+ | `mcp__db__sample_rows` | a few rows of a table |
60
+ | `mcp__db__execute_sql` | execute one statement — a query, or a write when that is allowed |
61
+ | `mcp__db__explain_query` | query plan |
62
+ | `mcp__db__get_ddl` | `CREATE TABLE` statement |
63
+ | `mcp__db__test_connection` | real login, `select 1`, server/user/schema |
64
+
65
+ `datasource` is optional on all of them: omitted, the default source is used.
66
+ Queries run **in the conversation**; the panel is for managing connections, not a
67
+ SQL console.
68
+
69
+ ## Safety
70
+
71
+ - **Read-only by default**, with two independent gates: a global write switch and a
72
+ per-source flag. Both must be open before a statement may write.
73
+ - **Passwords stay local.** They live in `secrets.json` (mode `0600`); no HTTP route
74
+ ever returns one, and the Python child receives a short environment allowlist
75
+ rather than the whole DSH environment.
76
+ - **Every tool call is audited** to `logs/audit.jsonl` — tool, source, duration, row
77
+ count, SQL. Result rows are never logged.
78
+ - **Nothing runs from the panel except catalog reads.** The structure browser only
79
+ forwards `list_tables` and `describe_table`.
80
+
81
+ ## Documentation
82
+
83
+ | Document | Contents |
84
+ | -------- | -------- |
85
+ | [docs/guide.md](docs/guide.md) | Panel walkthrough, connection strings, groups, Docker test databases, troubleshooting |
86
+ | [docs/reference.md](docs/reference.md) | HTTP routes, tool contracts, registry and settings files, plugin config |
87
+ | [docs/development.md](docs/development.md) | Repository layout, hot reload, test suites, release flow |
88
+ | [docs/design-notes.md](docs/design-notes.md) | Historical design log (P1 → P7) and the decisions behind them |
355
89
 
356
90
  ## License
357
91
 
358
92
  Apache License 2.0 — see [LICENSE](LICENSE).
359
-
360
- ## Runtime layout
361
-
362
- ```
363
- $DSH_HOME/db-mcp/
364
- datasources.json # profiles, never passwords, 0600
365
- secrets.json # per-source password, 0600, Host-only
366
- settings.json # global write switch + audit switch, 0600
367
- logs/mcp.log # masked log
368
- logs/audit.jsonl # one line per MCP tool call, rotates at 512 KiB
369
- .venv/ # plugin-local venv: mcp, psycopg, PyMySQL, oracledb
370
- ```