dsh-db-mcp 0.6.9 → 0.6.10

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,87 @@
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.
28
+ ## Run
24
29
 
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`).
167
-
168
- ```
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
-
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
- ```
183
-
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.
189
-
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`.
194
-
195
- ## Install into a profile
196
-
197
- ```
198
- plugin_manager install_bundle target=<absolute path to this directory>
199
- ```
200
-
201
- or, from npm:
202
-
203
- ```
30
+ ```sh
204
31
  dsh plugin add dsh-db-mcp@latest
205
32
  ```
206
33
 
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).
34
+ Then:
312
35
 
313
- ### What the plugin runs, and with what
36
+ 1. Sidebar → **Database**.
37
+ 2. **Import from DBeaver** → tick the connections → **Import selected**.
38
+ 3. Ask the agent something like *"which tables does my Chinook database have?"*
314
39
 
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`.
40
+ A connection whose password was never saved shows **no password**; open the source
41
+ once and type it — it goes to `secrets.json` and never comes back to the page.
327
42
 
328
- ## Audit log
43
+ Prefer to start from scratch? **New data source** takes one connection string
44
+ (`postgresql://user:pw@host:5432/db`, a JDBC URL, an Oracle TNS descriptor or a
45
+ `.db` path) and fills the form from it.
329
46
 
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.
47
+ ## What the agent gets
337
48
 
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) |
49
+ | Tool | Purpose |
50
+ | ---- | ------- |
51
+ | `mcp__db__list_datasources` | registered sources: id, kind, target, read-only |
52
+ | `mcp__db__list_tables` | tables and views of a source |
53
+ | `mcp__db__describe_table` | columns, types, nullability, primary key |
54
+ | `mcp__db__sample_rows` | a few rows of a table |
55
+ | `mcp__db__run_query` | run one statement |
56
+ | `mcp__db__explain_query` | query plan |
57
+ | `mcp__db__get_ddl` | `CREATE TABLE` statement |
58
+ | `mcp__db__test_connection` | real login, `select 1`, server/user/schema |
59
+
60
+ `datasource` is optional on all of them: omitted, the default source is used.
61
+ Queries run **in the conversation**; the panel is for managing connections, not a
62
+ SQL console.
63
+
64
+ ## Safety
65
+
66
+ - **Read-only by default**, with two independent gates: a global write switch and a
67
+ per-source flag. Both must be open before a statement may write.
68
+ - **Passwords stay local.** They live in `secrets.json` (mode `0600`); no HTTP route
69
+ ever returns one, and the Python child receives a short environment allowlist
70
+ rather than the whole DSH environment.
71
+ - **Every tool call is audited** to `logs/audit.jsonl` — tool, source, duration, row
72
+ count, SQL. Result rows are never logged.
73
+ - **Nothing runs from the panel except catalog reads.** The structure browser only
74
+ forwards `list_tables` and `describe_table`.
75
+
76
+ ## Documentation
77
+
78
+ | Document | Contents |
79
+ | -------- | -------- |
80
+ | [docs/guide.md](docs/guide.md) | Panel walkthrough, connection strings, groups, Docker test databases, troubleshooting |
81
+ | [docs/reference.md](docs/reference.md) | HTTP routes, tool contracts, registry and settings files, plugin config |
82
+ | [docs/development.md](docs/development.md) | Repository layout, hot reload, test suites, release flow |
83
+ | [docs/design-notes.md](docs/design-notes.md) | Historical design log (P1 → P7) and the decisions behind them |
355
84
 
356
85
  ## License
357
86
 
358
87
  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
- ```
package/README.zh.md CHANGED
@@ -1,252 +1,67 @@
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.md) | 中文
6
7
 
7
- 一个 DSH 插件:在侧边栏加一个 **数据库** 入口,配一个数据源管理面板,背后是 Python 写的 MCP 服务——让智能体通过工具查询这些数据库,而不是在页面里执行 SQL。
8
-
9
- - 一个面板管四种引擎:SQLite、PostgreSQL、MySQL、Oracle。
10
- - 一条连接串填满整张表单;口令只存在本机,永不回传给页面。
11
- - 可以把 DBeaver 里已有的连接(连同它加密保存的口令)直接导入。
12
- - 默认只读,两道互相独立的写闸门,加一份审计日志。
13
- - 给智能体的是 8 个 MCP 工具,界面里没有 SQL 编辑器。
14
-
15
- 需求与阶段计划见 [`docs/requirements.md`](docs/requirements.md)。
16
-
17
- ## 现在有什么(P7)
18
-
19
- - **Host 半边**(`index.js` → `src/host.js`):一个 Cordis 插件,通过 DSH 的 web server 提供 `/plugins/db-mcp/api`。
20
-
21
- | 路由 | 用途 |
22
- | ---- | ---- |
23
- | `GET /health` | 插件版本、Host 信息、Python 探测、运行时路径、注册表状态、MCP 状态、设置 |
24
- | `GET /datasources` | 注册表,口令字段已剔除 |
25
- | `GET /schema?datasource=…&table=…[&schema=…]` | 只读结构:表清单,或某张表的列 |
26
- | `GET /groups` | 已声明的分组,加上所有正在使用的分组名 |
27
- | `GET /dbeaver/scan` | DBeaver 里已配置的连接 |
28
- | `GET /mcp/status` | Python 服务所处阶段、工具清单、最后的错误、stderr 尾部 |
29
- | `GET /settings` · `POST /settings` | 全局写开关与审计开关 |
30
- | `GET /audit?limit=N` | 工具调用审计日志的末尾若干条 |
31
- | `GET /python/env` | 解释器、venv 路径、已安装的驱动版本 |
32
- | `POST /python/setup` | 建 venv 并 `pip install -r server/requirements.txt` |
33
- | `POST /datasources/save` | 新建或更新数据源(`id` 不可改) |
34
- | `POST /datasources/delete` | 删除数据源及其口令 |
35
- | `POST /datasources/test` | 真实登录探测:SQLite 走 `node:sqlite`,其余走 Python 驱动 |
36
- | `POST /datasources/bulk` | 对一批数据源做 测试 / 只读 / 分组 / 删除 |
37
- | `POST /groups/save` · `POST /groups/delete` | 新建、重命名、删除分组 |
38
- | `POST /dbeaver/import` | 把选中的 DBeaver 连接复制进注册表 |
39
- | `POST /mcp/start` · `POST /mcp/restart` | 启动或重启 Python MCP 服务 |
40
- | `POST /mcp/call` | 开发辅助:经面板调用某个 MCP 工具 |
41
- | `POST /reload` | 开发辅助:不重启 DSH 重新导入 `src/host.js` |
42
-
43
- - **Client 半边**(`client.js`):侧边栏入口(`db-mcp`,order 25)加主面板——数据源列表、极简的新建/编辑表单(一条连接串)、测试/删除/设为默认、只读的「结构」浏览、DBeaver 导入流程、「重启 MCP」按钮、运行时诊断卡(工具数、全局写开关、venv 与依赖版本及一键安装)、以及「审计日志」卡。文案经客户端 `locale` 服务做中英本地化。
44
- - **Python 服务**(`server/`):`main.py`(FastMCP,8 个工具)、`engines.py`(按引擎连接/内省/查询)、`store.py`(读注册表)。插件用 stdio 拉起它,把它的工具以 `mcp__db__*` 发布到 Host 的 `tools` 服务上——**不**经过 `dsh-mcp-client` 的 Loader 行。
45
-
46
- ### 智能体拿到的工具
47
-
48
- | 工具 | 用途 |
49
- | ---- | ---- |
50
- | `mcp__db__list_datasources` | 已登记的数据源(id、类型、目标、是否只读) |
51
- | `mcp__db__list_tables` | 某个数据源的表/视图 |
52
- | `mcp__db__describe_table` | 列、类型、是否可空、主键 |
53
- | `mcp__db__sample_rows` | 某张表的少量样本 |
54
- | `mcp__db__run_query` | 执行一条语句;只读源拒绝写 |
55
- | `mcp__db__explain_query` | 查询计划 |
56
- | `mcp__db__get_ddl` | `CREATE TABLE` 语句(SQLite 取原文,其余按目录重建) |
57
- | `mcp__db__test_connection` | 真实登录、执行 `select 1`、回报服务器/用户/模式 |
58
-
59
- `datasource` 处处可选:不填就用默认数据源。
60
-
61
- ### Python 环境
62
-
63
- 解释器探测顺序:插件配置 `pythonPath` → `<DSH_HOME>/db-mcp/.venv/Scripts/python.exe` → POSIX 上的同一个 venv → `python` → `python3`。依赖装在运行时 venv 里,不随包分发:
64
-
65
- python -m venv C:\Users\<you>\.dsh\db-mcp\.venv
66
- C:\Users\<you>\.dsh\db-mcp\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
67
-
68
- `mcp` 必须固定在 1.x(2.x 把 `FastMCP` 改名成了 `MCPServer`)。
69
-
70
- ## 「测试」按钮是真的登录吗?
71
-
72
- 是。PostgreSQL、MySQL、Oracle 的 **测试** 走 `mcp__db__test_connection`,经 Python MCP 服务执行一次真实认证登录加 `select 1`(Oracle 用 `select 1 from dual`)。列表行随后显示服务器横幅、登录用户与库名、以及登录耗时,例如:
73
-
74
- ```
75
- Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok
76
- PostgreSQL 15.19 · demo@demo · select version() ok
77
- MySQL 8.0.46 · demo@%@demo · select version() ok
78
- ```
79
-
80
- 口令错误或端口不通时,显示的是驱动自己的信息(`ORA-01017`、`DPY-6005`、`password authentication failed`),而不是笼统的「端口可达」。未保存的表单也能测:`test_connection` 接受 `kind` + `config` + `password`,不需要先写注册表。Python 服务起不来时,探测退回 TCP 检查,并明确说明这一点。
81
-
82
- ## 一条连接串填完整张表单
83
-
84
- 新建/编辑表单刻意做得和 DBeaver 的连接对话框一样小:**连接串**、**用户**、**口令**(带「保存密码」勾选),就这些。粘贴 JDBC URL、DSN、Oracle TNS 描述符或 `.db` 路径,类型、主机、端口、库名/服务名、名称与标识符都会自动带出;推导出来的值默认收在「更多设置」里,与模式、SID、SSL 模式、字符集、只读开关放在一起。
85
-
86
- 支持的写法(完整矩阵见 `dev/parse.test.mjs`):
87
-
88
- ```
89
- postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
90
- jdbc:postgresql://localhost:5432/appdb
91
- mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
92
- jdbc:mariadb://10.0.0.5:3307/legacy
93
- oracle://system:oracle@localhost:1521/XEPDB1
94
- jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # 服务名
95
- jdbc:oracle:thin:@ora:1521:ORCL # SID
96
- (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
97
- jdbc:sqlite:C:\data\app.db
98
- sqlite:///C:/data/chinook.db
99
- C:\data\app.db / /var/lib/app.sqlite3
100
- ```
101
-
102
- 连接串里的口令只发送一次给 `POST /datasources/save`,之后只存在 `secrets.json` 里,永不回显。
103
-
104
- 写操作一律用 `POST`(同源校验比较 `Origin` 与 `Host`)。口令不会进 `datasources.json`,只存在 `secrets.json`,页面拿到的唯一信号是 `hasSecret`。
105
-
106
- ## 从 DBeaver 导入
107
-
108
- `GET /dbeaver/scan` 会在 `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\` 下找 `data-sources.json`(同时兼容 `General` 布局与 Linux/macOS 位置)。同级存在 `credentials-config.json` 时,用 DBeaver 内置的密钥解密(AES-128-CBC、零 IV、前 16 字节前缀,参考 `Tatsh/dbeaver-creds`)。从未保存过口令的工作区,导入出来的连接口令为空,界面显示「无密码」。
109
-
110
- 通过 DBeaver 驱动对话框建的连接,往往只有一条 JDBC URL(`configurationType: URL`),既没有 `database` 字段也没有 `user` 字段。导入器因此会自己解析 `jdbc:mysql://host:port/db`、`jdbc:postgresql://…`、`jdbc:oracle:thin:@…`,补上表单字段缺的部分,并在解密凭据里存了用户名时从那里取。导入的数据源按 DBeaver 连接名命名(再 slug 成 id),保持 `readOnly: true`,已存的口令会复制进 `secrets.json`;仍然缺的东西写进 `warnings`。
111
-
112
- 已经配置过的连接不会被导入两次:每个数据源用 类型 + 主机 + 端口 + 库名/服务名/SID + 用户 做身份(SQLite 用文件路径),命中的 DBeaver 条目会带上 `alreadyImported: true`、`duplicateId`、`duplicateName` 返回。面板把它的复选框禁用并标注「已作为 … 添加」,`POST /dbeaver/import` 则把这类 id 放进 `skipped`,不会建出第二份。
113
-
114
- 面板用标签栏导航(*数据源列表 / 新建数据源 / 从 DBeaver 导入 / 运行时诊断 / 审计日志*):当前步骤是纯文本、可回退的步骤是链接,任何页面都不需要额外的「返回列表」按钮。
115
-
116
- ## 用 Docker 起测试库
117
-
118
- 宿主机什么都不用装,下面每个引擎都是用完即弃的容器,演示账号 `demo` / `dbmcp-demo`。
119
-
120
- ```
121
- # PostgreSQL —— 镜像最小、启动最快,推荐第一个试
122
- docker run -d --name dbmcp-pg -p 5432:5432 \
123
- -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
124
- postgres:15-alpine
125
-
126
- # MySQL
127
- docker run -d --name dbmcp-mysql -p 3306:3306 \
128
- -e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
129
- -e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
130
-
131
- # Oracle Free —— 镜像好几个 GB,要一两分钟才就绪
132
- docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
133
- gvenzl/oracle-free:23-slim
134
- ```
8
+ 一个 [DSH](https://github.com/deepseek-ai/deepseek-harness) 插件,把数据库交给智能体:侧栏多出一个**数据库**面板,登记过的每个数据源都会变成 `mcp__db__*` 工具。
135
9
 
136
- Windows 上 1521 常常落在 Hyper-V 的保留端口段里,`docker run` 会报「尝试访问套接字的方式被其访问权限禁止」。用 `netsh interface ipv4 show excludedportrange protocol=tcp` 查一下,换一个宿主端口发布即可,例如 `-p 11521:1521`,数据源里填 11521。
10
+ **零配置。** 不需要改 DSH 里的任何文件,也不用手工抄一遍连接信息——直接把 DBeaver 里已有的连接导进来,然后开口问就行。
137
11
 
138
- 然后在面板里用对应的连接串建数据源,例如
139
- `postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo`、
140
- `mysql://demo:dbmcp-demo@127.0.0.1:3306/demo` 或
141
- `oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1`。
12
+ ## 三个方面都不用配
142
13
 
143
- ## 装进一个 profile
14
+ - **不用改 DSH 文件。** bundle 的 patch 只插入一行 Loader 记录;插件自己用 stdio 拉起 Python MCP 服务,并把工具注册到 Host 的 `tools` 服务上——不需要 `dsh-mcp-client` 那一行,也不用编辑 JSON、导环境变量。
15
+ - **不用填数据源。** 导入器读取 DBeaver 工作区的 `data-sources.json`,再用 DBeaver 自带的密钥解密 `credentials-config.json`,主机、端口、库名、用户、已存口令一次到位。勾一个框就是全部配置工作。
16
+ - **不用准备 Python。** 面板会自己找解释器、建 venv、装四个驱动包,入口在 **运行时诊断 → 安装 / 修复依赖**。
144
17
 
145
- ```
146
- plugin_manager install_bundle target=<本目录的绝对路径>
147
- ```
18
+ ## 运行
148
19
 
149
- 或者从 npm 装:
150
-
151
- ```
20
+ ```sh
152
21
  dsh plugin add dsh-db-mcp@latest
153
22
  ```
154
23
 
155
- bundle patch(`cordis.patch.yml`)只插入一行 Loader 配置,id 为 `db-mcp`。以链接方式装入的 profile 改文件即生效;Host 模块每个进程只导入一次,所以改完 `src/host.js` 要 `POST /plugins/db-mcp/api/reload`(或重启应用)。
156
-
157
- ## 面板布局
158
-
159
- 页面与其它官方面板保持一致的外壳:
160
-
161
- - 页头:**‹ 返回会话**(经 `ctx.layout.selectPanel(null)` 关闭面板)、**数据库** 标题,以及连接状态与版本药丸;
162
- - 标签栏 —— **数据源列表 / 新建数据源 / 从 DBeaver 导入 / 运行时诊断 / 审计日志**,当前标签带下划线。
163
-
164
- 点「新建数据源」或「从 DBeaver 导入」只切换标签下方那张卡片,页头与标签栏不动。编辑复用「新建数据源」标签;结构浏览是「数据源列表」下的子视图,带自己的 `← 数据源` 链接。没有第二排操作栏:**刷新** 与 **重启 MCP** 在数据源列表卡片的头部,分组相关的操作归列表自己管。
165
-
166
- ## 结构概览
167
-
168
- 数据源行上的「结构」按钮打开只读浏览器:左边是该源的表(或视图),右边是选中表的列——名称、类型、可空、默认值、主键标记。数据由 `GET /schema` 提供,它只把 `list_tables` / `describe_table` 转发给 MCP 服务,页面本身永远执行不了 SQL。不缓存:每次打开都读实时目录。
169
-
170
- ## 分组与批量操作
171
-
172
- 每个数据源带一个可选的**分组**标签(注册表里的 `group`,可在「更多设置」里改)。分组归用户所有:在「新建分组」框里输入名字即可创建,**双击组名改名**(或按删除按钮删组),把数据源行拖到某个组上即可移过去(拖到「未分组」上则清除标签)。双击数据源名只改这一条的名字,id 永不改变。列表按组渲染成若干区块,组名排序、未分组的排最后;没有分组时列表保持原来的平铺样式。
173
-
174
- 每个组头都有全选复选框,所以组内的数据源和散在外面的完全一样,都能参与批量操作。勾选任意行即出现批量操作条:批量测试、设为只读或允许写、移动到某个分组、或删除(两步确认)。批量移动走 `POST /datasources/bulk`,body 形如 `{ action: 'test' | 'readOnly' | 'group' | 'delete', ids: [...], … }`;分组的增删改走 `GET /groups`、`POST /groups/save`、`POST /groups/delete`。
175
-
176
- ## 发布
177
-
178
- 包由 GitHub Actions 通过 [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) 发布:`.github/workflows/publish.yml` 用 workflow 的 OIDC token 换短期发布凭据,因此仓库里没有任何 `NPM_TOKEN` 密钥。
179
-
180
- ```bash
181
- node dev/bump.mjs 0.6.5 # 同时改 package.json 与 src/host.js
182
- git commit -am "chore: release 0.6.5"
183
- git tag v0.6.5 && git push origin main --tags
184
- ```
185
-
186
- workflow 会拒绝 tag 与 `package.json` 版本不一致的情况,先跑可移植测试(`npm run test:ci`),再带 `--provenance` 发布,最后轮询 registry 确认版本已出现。首次发布(包还没有 trusted publisher 时)仍可本地 `npm publish`。
187
-
188
- ## 开发
189
-
190
- ```
191
- node dev/engines.test.mjs # 11 项:engines.py 的 commit/rollback 与 affectedRows,用假连接
192
- node dev/selftest.mjs # 在临时端口挂载 src/host.js,46 项断言
193
- node dev/shims.test.mjs # 挂载 index.js,检查路由、/reload、工具发布
194
- node dev/parse.test.mjs # 24 项:连接串解析(DSN/JDBC/TNS/路径)
195
- node dev/render.test.mjs # 40 项:用 stub React 渲染组件与结果渲染器
196
- node dev/i18n.test.mjs # 12 项:中英键集合一致、无死键、无漏译
197
- node dev/p5.test.mjs # 15 项:设置、审计日志、Python 环境
198
- node dev/mcp.test.mjs # 19 项:真实 Python 服务走 stdio,真实 SQLite 与 Oracle 登录
199
- ```
200
-
201
- `npm test` 跑除 `mcp.test.mjs` 之外的全部(后者需要 venv 与 Oracle 容器)。`dev/engines.test.mjs` 只需要一个 Python 解释器(插件 venv,否则 PATH 里的 `python`),不连数据库:它用假 DB-API 连接驱动 `server/engines.py`,守住 `run_query` 的 commit/rollback 与 `affectedRows` 契约。`node dev/bump.mjs 0.6.6` 会同步两处版本号。
24
+ 然后:
202
25
 
203
- ## 安全闸门
26
+ 1. 侧栏 → **数据库**。
27
+ 2. **从 DBeaver 导入** → 勾选连接 → **导入所选**。
28
+ 3. 问智能体一句:*「我的 Chinook 库里有哪些表?」*
204
29
 
205
- 是否允许写入由两道互相独立的闸门决定:
30
+ 如果某条连接在 DBeaver 里没保存口令,卡片会显示**无密码**;打开该数据源补一次即可——口令只写进 `secrets.json`,不会再回到页面上。
206
31
 
207
- 1. **全局开关** —— `<运行时目录>/settings.json` 的 `allowWrite`,在诊断卡上切换。默认关闭;关闭期间所有写语句都以 `user_error` 拒绝,并给出指向该开关的提示。
208
- 2. **单源标记** —— 数据源的 `readOnly`,默认开启。即使全局开关打开,只要不清掉这条标记,该源仍然只读。点数据源行上的「只读 / 可写」药丸即可一键切换:该源可写时药丸变成琥珀色,若全局开关仍关着,提示条会提醒你去打开它。
32
+ 想从零开始也行:**新建数据源**里粘一条连接串(`postgresql://user:pw@host:5432/db`、JDBC URL、Oracle TNS 描述符或 `.db` 路径),表单会自动填好。
209
33
 
210
- Python 服务启动时从环境变量 `DB_MCP_ALLOW_WRITE` 读取全局标记;开关一变,Host 就会重启它。只要引擎支持,只读还会在驱动层再兜一次(SQLite 的 `mode=ro`、PostgreSQL 的 `default_transaction_read_only`、MySQL 的 `SET SESSION TRANSACTION READ ONLY`)。
211
-
212
- ### 插件会启动什么,以及带着什么
213
-
214
- - Python 服务用固定的解释器路径、固定脚本、固定参数启动,**从不经过 shell**,所以任何字符串都不会被 `cmd.exe` 或 `/bin/sh` 再解析一次。
215
- - 子进程**不继承** Host 的环境变量。它只拿到一张很短的允许清单(`PATH`、`TEMP`、`HOME`、`PYTHON*` 之类)再加上插件自己设置的 `DB_MCP_*`。DSH 进程里的 token、云凭据不会被带下去。
216
- - PostgreSQL / MySQL / Oracle 都由这个子进程连接,只有 SQLite 是在进程内读的。裸 TCP 探测只是子进程起不来时的兜底,且不发任何凭据。
217
- - 口令存在 `secrets.json`(`0600`),任何 HTTP 接口都不会返回它;面板只知道 `hasSecret`。
34
+ ## 智能体拿到的工具
218
35
 
36
+ | 工具 | 作用 |
37
+ | ---- | ---- |
38
+ | `mcp__db__list_datasources` | 已登记的数据源:id、类型、目标、是否只读 |
39
+ | `mcp__db__list_tables` | 某个源里的表与视图 |
40
+ | `mcp__db__describe_table` | 列、类型、可空、主键 |
41
+ | `mcp__db__sample_rows` | 取几行样本 |
42
+ | `mcp__db__run_query` | 执行一条语句 |
43
+ | `mcp__db__explain_query` | 执行计划 |
44
+ | `mcp__db__get_ddl` | `CREATE TABLE` 语句 |
45
+ | `mcp__db__test_connection` | 真实登录 + `select 1`,回报服务器/用户/schema |
219
46
 
220
- ## 审计日志
47
+ `datasource` 在所有工具里都是可选的,省略就用默认数据源。查询发生在**对话里**,面板只负责管理连接,不是 SQL 控制台。
221
48
 
222
- 每次 MCP 工具调用都会向 `<运行时目录>/logs/audit.jsonl`(0600)追加一行 JSON:时间、工具、数据源、耗时、行数、截断到 2000 字符的 SQL,以及失败时的错误。结果行永不记录。文件超过 512 KiB 轮转为 `audit.1.jsonl`,也可以在 `settings.json` 里用 `auditLog: false` 关掉。面板显示最近十几条,时间按**本机时区**换算——文件里存的是 UTC,这样换机器也仍然可比;卡片会把每条时间戳转成浏览者的时区,并标出所用偏移。
49
+ ## 安全
223
50
 
224
- `dev/shims.test.mjs` 还断言 `package.json` 不带 UTF-8 BOM:Host 用 `JSON.parse` 读它,而 `JSON.parse` 会拒绝 BOM,插件随之显示为 `availability: missing`,并不加载。会「添加 BOM」的编辑器会静默弄坏 bundle——如果重启后侧边栏入口消失,先查这一项。
51
+ - **默认只读**,两道互相独立的闸门:全局写开关 + 单源标记,两者都打开才允许写入。
52
+ - **口令不出本机。** 存在 `secrets.json`(`0600`);任何 HTTP 接口都不返回它,Python 子进程拿到的也只是很短的环境变量白名单,而不是整个 DSH 环境。
53
+ - **每次工具调用都有审计**,写入 `logs/audit.jsonl`——工具、数据源、耗时、行数、SQL;结果行永不记录。
54
+ - **面板不会执行 SQL。** 结构浏览只转发 `list_tables` 与 `describe_table`。
225
55
 
226
- ## 阶段表
56
+ ## 文档
227
57
 
228
- | 阶段 | 内容 |
58
+ | 文档 | 内容 |
229
59
  | ---- | ---- |
230
- | P1 | 可安装的骨架:侧边栏入口、空面板、Host 健康接口(已完成) |
231
- | P2 | 数据源 CRUD、连接测试、DBeaver 导入(已完成) |
232
- | P3 | Python MCP 服务,工具以 `mcp__db__*` 发布(已完成) |
233
- | P4 | PostgreSQL / MySQL / Oracle 方言(已完成) |
234
- | P5 | 诊断、一键 venv、热重载、审计日志、全局写开关(已完成) |
235
- | P6 | 多语言打磨与文档(已完成) |
236
- | P7 | 只读结构概览、分组、批量操作(已完成) |
60
+ | [docs/guide.md](docs/guide.md) | 面板操作、连接串、分组、Docker 测试库、排错(英文) |
61
+ | [docs/reference.md](docs/reference.md) | HTTP 接口、工具契约、注册表与设置文件、插件配置(英文) |
62
+ | [docs/development.md](docs/development.md) | 目录结构、热重载、测试套件、发布流程(英文) |
63
+ | [docs/design-notes.md](docs/design-notes.md) | 设计日志(P1 → P7)与决策记录(英文) |
237
64
 
238
65
  ## 许可
239
66
 
240
67
  Apache License 2.0 —— 见 [LICENSE](LICENSE)。
241
-
242
- ## 运行时目录
243
-
244
- ```
245
- $DSH_HOME/db-mcp/
246
- datasources.json # 数据源档案,不含口令,0600
247
- secrets.json # 各源口令,0600,仅 Host 可读
248
- settings.json # 全局写开关 + 审计开关,0600
249
- logs/mcp.log # 已掩码的日志
250
- logs/audit.jsonl # 每次 MCP 工具调用一行,超过 512 KiB 轮转
251
- .venv/ # 插件内 venv:mcp、psycopg、PyMySQL、oracledb
252
- ```
@@ -1,3 +1,8 @@
1
+ > **Design log.** The working record of the design and the P1 → P7 build, written in
2
+ > Chinese as the project ran. It is kept for the decisions and the edge cases behind
3
+ > them, not as user documentation: start at [../README.md](../README.md), or
4
+ > [guide.md](guide.md) for usage and [reference.md](reference.md) for the wire format.
5
+
1
6
  # DSH 数据库插件(暂名 `dsh-db-mcp`)需求细化 v0.2
2
7
 
3
8
  > 状态:需求已按你的选择定稿,代码尚未开始
@@ -0,0 +1,99 @@
1
+ # Development
2
+
3
+ How the repository is put together, how to iterate on it without restarting DSH
4
+ every time, and how a release happens.
5
+
6
+ ## Layout
7
+
8
+ ```
9
+ index.js Cordis entry: routes only, plus the cache-busting import of src/host.js
10
+ src/host.js Host half: registry, DBeaver import, probes, settings, audit, MCP bridge wiring
11
+ src/mcp.js minimal MCP client over stdio (newline-delimited JSON-RPC)
12
+ src/tools.js spawns the Python server, publishes its tools on the host tools service
13
+ client.js browser half: sidebar entry + panel (React, theme tokens only)
14
+ locale/{en,zh}.json package metadata (title/description) shown by the plugin card
15
+ server/main.py FastMCP server: eight tools
16
+ server/engines.py per-engine connect / introspect / query / DDL
17
+ server/store.py reads the registry written by the Host
18
+ cordis.patch.yml one Loader row with id "db-mcp"
19
+ dev/ test suites and the version bump script
20
+ docs/ this documentation
21
+ ```
22
+
23
+ The package ships `index.js`, `src/*.js`, `client.js`, `locale/*.json`, `icon.svg`,
24
+ `cordis.patch.yml`, `server/*.py`, `server/requirements.txt`, `docs/*.md`, both
25
+ READMEs and the license.
26
+
27
+ ## Hot reload
28
+
29
+ The Cordis Loader imports a plugin entry **once per process**, so editing
30
+ `index.js` needs a DSH restart. Everything else is deliberately reloadable:
31
+
32
+ - `index.js` only registers two routes and imports `./src/host.js?v=<n>`, so
33
+ `POST /plugins/db-mcp/api/reload` re-imports the whole Host half.
34
+ - `src/host.js` and `src/tools.js` propagate the same generation string to their own
35
+ imports (`new URL(import.meta.url).searchParams.get('v')`), otherwise Node's module
36
+ cache would keep returning the old copies of the inner files.
37
+ - The Python server is a child process: `POST /mcp/restart` (or the **Restart MCP**
38
+ button) replaces it. Tool registrations stay in place — disposing and re-registering
39
+ them makes the agent's tool catalog go stale and report `unknown tool`.
40
+
41
+ A profile that links this directory (`dsh plugin add <path>`) reflects edits
42
+ immediately; one installed from npm needs a release.
43
+
44
+ ## Tests
45
+
46
+ ```sh
47
+ npm test # everything below except mcp.test.mjs
48
+ npm run test:ci # the portable subset the publish workflow runs
49
+ npm run test:mcp # real Python server, real SQLite; Oracle when its container is up
50
+ npm run test:engines # server/engines.py against a stub DB-API connection
51
+ ```
52
+
53
+ | Suite | Covers |
54
+ | ----- | ------ |
55
+ | `dev/engines.test.mjs` + `engines.test.py` | commit/rollback and `affectedRows` contract of `run_query`, no database needed |
56
+ | `dev/selftest.mjs` | mounts `src/host.js` on a throwaway port: registry, validation, groups, bulk, schema route, DBeaver scan, probes |
57
+ | `dev/shims.test.mjs` | mounts `index.js`, checks both routes, `/reload`, tool publication, and that `package.json` has no BOM |
58
+ | `dev/parse.test.mjs` | the connection-string parser (DSN, JDBC, TNS, SID, paths) |
59
+ | `dev/render.test.mjs` | components rendered with a stub React: tab bar, form fields, list, bulk bar, breadcrumbs |
60
+ | `dev/i18n.test.mjs` | en/zh key parity, no dead keys, no stray copy outside the dictionaries |
61
+ | `dev/p5.test.mjs` | settings, audit rotation, Python environment discovery |
62
+ | `dev/mcp.test.mjs` | real stdio handshake, all eight tools, read-only refusal, login failures |
63
+
64
+ Suites that need external services skip rather than fail: `mcp.test.mjs` probes the
65
+ Oracle port and prints `skip … is not listening` when the container is down.
66
+
67
+ ## Releasing
68
+
69
+ The package publishes from GitHub Actions with
70
+ [npm trusted publishing](https://docs.npmjs.com/trusted-publishers):
71
+ `.github/workflows/publish.yml` trades the workflow's OIDC token for a short-lived
72
+ publish credential, so no `NPM_TOKEN` secret exists.
73
+
74
+ ```sh
75
+ node dev/bump.mjs 0.6.10 # package.json + src/host.js, both
76
+ git commit -am "chore: release 0.6.10"
77
+ git push origin main
78
+ git tag v0.6.10 && git push origin v0.6.10
79
+ ```
80
+
81
+ The workflow refuses to publish when the tag and `package.json` disagree, runs
82
+ `npm run test:ci`, publishes with `--provenance`, then polls the registry until the
83
+ version appears. Local `npm publish` still works for the very first release, before
84
+ a trusted publisher exists.
85
+
86
+ Keep `src/host.js`'s `PLUGIN_VERSION` in step with `package.json` — the panel and
87
+ `/health` read it, and a mismatch ships a package that reports the previous version.
88
+
89
+ ## Adding an engine
90
+
91
+ 1. `server/engines.py`: extend `connect()` and the dialect tables (`_quote`,
92
+ `_limit_clause`, `list_tables`, `describe_table`, `table_ddl`).
93
+ 2. `src/host.js`: add the kind to `KINDS`, give it a default port, a validation
94
+ branch in `normalizeInput`, and a `describeTarget` label.
95
+ 3. `client.js`: add the string to `KINDS` and, if the form needs extra fields, to
96
+ `SourceForm`; extend `parseConnectionText` with the scheme and JDBC prefix.
97
+ 4. `server/requirements.txt`: the driver, and `docs/reference.md`: the field table.
98
+ 5. Tests: `dev/parse.test.mjs` for the string, `dev/selftest.mjs` for validation and
99
+ the probe, and a demo container in `docs/guide.md`.
package/docs/guide.md ADDED
@@ -0,0 +1,223 @@
1
+ # Guide
2
+
3
+ Everything the panel can do, in the order you meet it. For the wire protocol and
4
+ file formats see [reference.md](reference.md); for build and release see
5
+ [development.md](development.md).
6
+
7
+ ## Install and upgrade
8
+
9
+ ```sh
10
+ dsh plugin add dsh-db-mcp@latest # fresh install
11
+ dsh plugin add dsh-db-mcp@0.6.10 # pin a version (skips dist-tag and cache games)
12
+ ```
13
+
14
+ From a source checkout, install the directory instead — the profile then links it,
15
+ so edits are live:
16
+
17
+ ```sh
18
+ dsh plugin add C:\path\to\dsh-db-mcp
19
+ ```
20
+
21
+ Two things to know about upgrades:
22
+
23
+ - pnpm caches a package's metadata. A version published minutes ago may still be
24
+ invisible to `@latest`; naming the version sidesteps it.
25
+ - DSH applies a supply-chain cooldown to freshly published releases. Pinning is
26
+ also the way around that.
27
+
28
+ If the sidebar entry disappears after a restart, see [Troubleshooting](#troubleshooting).
29
+
30
+ ## The panel
31
+
32
+ Sidebar → **Database**. The masthead carries **‹ Back to chat**, the title, and the
33
+ connection and version pills. Below it, five tabs:
34
+
35
+ | Tab | What it does |
36
+ | ---- | ------------ |
37
+ | Data source list | the registry: rows grouped by label, bulk actions, refresh, restart MCP |
38
+ | New data source | one connection string, user, password; also used for editing |
39
+ | Import from DBeaver | scan, tick, import |
40
+ | Runtime | Host facts, tool count, global write switch, venv and driver versions |
41
+ | Audit log | the last dozen tool calls, converted to your time zone |
42
+
43
+ **New data source** and **Import from DBeaver** swap only the card under the tabs —
44
+ the masthead and tabs stay put, and every earlier step is still a tab away.
45
+
46
+ ## Adding a source
47
+
48
+ The form is as small as DBeaver's connection dialog: **connection string**, **user**,
49
+ **password** (with a *save password* box). Pasting fills type, host, port,
50
+ database/service, name and identifier; the derived values only appear under
51
+ **More settings**, together with schema, SID, SSL mode, charset and the read-only
52
+ switch.
53
+
54
+ Accepted shapes — a DSN, a JDBC URL, an Oracle TNS descriptor, a JDBC thin
55
+ descriptor, or a bare database file:
56
+
57
+ ```
58
+ postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
59
+ jdbc:postgresql://localhost:5432/appdb
60
+ mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
61
+ jdbc:mariadb://10.0.0.5:3307/legacy
62
+ oracle://system:oracle@localhost:1521/XEPDB1
63
+ jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # service name
64
+ jdbc:oracle:thin:@ora:1521:ORCL # SID
65
+ (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
66
+ jdbc:sqlite:C:\data\app.db
67
+ sqlite:///C:/data/chinook.db
68
+ C:\data\app.db / /var/lib/app.sqlite3
69
+ ```
70
+
71
+ A password found inside the URL is sent once and then only stored; it is never
72
+ echoed back. `id` is immutable once created — rename the *display name* instead
73
+ (double-click the name on a row).
74
+
75
+ ## Importing from DBeaver
76
+
77
+ The importer looks for `data-sources.json` under
78
+ `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\`, plus the `General` and
79
+ Linux/macOS layouts. When the sibling `credentials-config.json` exists it is
80
+ decrypted with DBeaver's built-in key (AES-128-CBC, zero IV, 16-byte prefix — see
81
+ [Tatsh/dbeaver-creds](https://github.com/Tatsh/dbeaver-creds)). Ticked entries are
82
+ copied into the registry, their passwords into `secrets.json`.
83
+
84
+ Two details make real DBeaver workspaces work:
85
+
86
+ - Connections created through the driver dialog often carry only a JDBC URL
87
+ (`configurationType: URL`) with no `database` field and no `user`. The importer
88
+ parses the URL for whatever the form fields omit and takes the user from the
89
+ decrypted credentials.
90
+ - A connection already configured is not offered twice. Sources are identified by
91
+ kind + host + port + database/service/SID + user (SQLite by file path); a match
92
+ comes back as `alreadyImported`, the checkbox is disabled and the row says
93
+ *already added as …*.
94
+
95
+ A workspace that never saved passwords is fine: the connection is imported with an
96
+ empty password and the row shows **no password**. Fill it in once and it stays.
97
+
98
+ ## Testing a connection
99
+
100
+ **Test** performs a real login: SQLite is opened read-only through `node:sqlite`,
101
+ the other engines go through the Python drivers, which authenticate and run
102
+ `select 1` (Oracle: `select 1 from dual`). The row then shows the banner, the user
103
+ and database, and how long the login took:
104
+
105
+ ```
106
+ Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok
107
+ PostgreSQL 15.19 · demo@demo · select version() ok
108
+ MySQL 8.0.46 · demo@%@demo · select version() ok
109
+ ```
110
+
111
+ Wrong passwords now surface the driver's own message (`ORA-01017`, `DPY-6005`,
112
+ `password authentication failed`) instead of "port reachable". An unsaved form can
113
+ be tested too. When the Python server cannot start, the probe degrades to a TCP
114
+ check and says so.
115
+
116
+ ## Groups and bulk actions
117
+
118
+ Every source carries an optional **group** label, editable under *More settings*.
119
+
120
+ - **Create** — type a name in **New group**.
121
+ - **Rename** — double-click the group name; **delete** drops the label from its
122
+ members.
123
+ - **Move** — drag a source row onto a group, or onto **Ungrouped** to clear it.
124
+ - **Rename a source** — double-click its name; the id never changes.
125
+ - **Select** — each group header has a select-all box, so grouped sources join bulk
126
+ work exactly like loose ones.
127
+
128
+ With a selection made, the bulk bar can test, flip read-only, move into a group, or
129
+ delete (two-step confirmation).
130
+
131
+ ## Structure browser
132
+
133
+ **Structure** on a source row opens a read-only view: tables and views on the left,
134
+ columns of the selected table on the right (name, type, nullability, default,
135
+ primary-key marker). It is served by `GET /schema`, which only forwards
136
+ `list_tables` / `describe_table` to the MCP server — the page cannot run SQL, and
137
+ nothing is cached.
138
+
139
+ ## Safety switches
140
+
141
+ Two independent gates decide whether a statement may write:
142
+
143
+ 1. **Global switch** — `allowWrite` in `<runtime>/settings.json`, toggled on the
144
+ *Runtime* tab. Off by default; while off every write is refused with
145
+ `user_error` and a hint pointing at the switch.
146
+ 2. **Per-source flag** — `readOnly` on the source, on by default. Click the
147
+ *read-only* / *writable* pill on a row to flip it in one click; the pill turns
148
+ amber while that source accepts writes.
149
+
150
+ The Python server reads the global flag at start-up and the host restarts it
151
+ whenever the switch flips. Read-only is enforced again at the driver where the
152
+ engine allows it (`mode=ro` for SQLite, `default_transaction_read_only` for
153
+ PostgreSQL, `SET SESSION TRANSACTION READ ONLY` for MySQL).
154
+
155
+ ## Audit log
156
+
157
+ Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
158
+ time, tool, source, duration, row count, the SQL truncated to 2000 characters, and
159
+ the error when it failed. Result rows are never logged. The file rotates to
160
+ `audit.1.jsonl` at 512 KiB and logging can be switched off with `auditLog: false`.
161
+
162
+ The panel shows the last dozen entries **on your own clock**; the file keeps UTC so
163
+ it stays comparable across machines. Each stamp carries the full local timestamp in
164
+ its tooltip.
165
+
166
+ ## Runtime layout
167
+
168
+ ```
169
+ $DSH_HOME/db-mcp/
170
+ datasources.json # sources and groups, never passwords, 0600
171
+ secrets.json # per-source password, 0600, Host-only
172
+ settings.json # global write switch + audit switch, 0600
173
+ logs/mcp.log # masked log
174
+ logs/audit.jsonl # one line per MCP tool call, rotates at 512 KiB
175
+ .venv/ # plugin-local venv: mcp, psycopg, PyMySQL, oracledb
176
+ ```
177
+
178
+ ## Test databases in Docker
179
+
180
+ Nothing has to be installed on the host; each engine below is a throwaway
181
+ container with a demo account (`demo` / `dbmcp-demo`).
182
+
183
+ ```sh
184
+ # PostgreSQL — smallest and fastest, the recommended first target
185
+ docker run -d --name dbmcp-pg -p 5432:5432 \
186
+ -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
187
+ postgres:15-alpine
188
+
189
+ # MySQL
190
+ docker run -d --name dbmcp-mysql -p 3306:3306 \
191
+ -e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
192
+ -e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
193
+
194
+ # Oracle Free — several GB, a minute or two to become healthy
195
+ docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
196
+ gvenzl/oracle-free:23-slim
197
+ ```
198
+
199
+ On Windows, 1521 often falls inside a Hyper-V reserved port block and `docker run`
200
+ fails with *"An attempt was made to access a socket in a way forbidden by its access
201
+ permissions"*. Check `netsh interface ipv4 show excludedportrange protocol=tcp` and
202
+ publish another host port, e.g. `-p 11521:1521`; point the datasource at 11521.
203
+
204
+ Then import or create a source with the matching string:
205
+
206
+ ```
207
+ postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo
208
+ mysql://demo:dbmcp-demo@127.0.0.1:3306/demo
209
+ oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1
210
+ ```
211
+
212
+ ## Troubleshooting
213
+
214
+ | Symptom | Cause | Fix |
215
+ | ------- | ----- | --- |
216
+ | Sidebar entry gone, plugin card says *not installed* | `package.json` carries a UTF-8 BOM, so the host's `JSON.parse` fails and the bundle is skipped silently | rewrite the file without a BOM; re-apply with `plugin_manager set_bundle` |
217
+ | Panel says *Host interface disconnected* | the Host half is not mounted (plugin disabled, or bundle failed to load) | check the plugin card, then reload the page |
218
+ | Edited `src/host.js` but nothing changed | the Cordis entry is imported once per process | `POST /plugins/db-mcp/api/reload`, or restart DSH after changing `index.js` |
219
+ | Tools missing from the agent | the Python server never started | *Runtime* → tools count and error; then **Restart MCP** |
220
+ | `unknown tool "mcp__db__…"` right after a restart | tool registration was disposed with the old process | restart DSH once, then drive restarts from **Restart MCP** |
221
+ | Oracle login fails with `DPY-6005` | listener unreachable, or host/port wrong | `docker ps`, then *Test* again |
222
+ | `ORA-01950` on first write | Oracle user has no tablespace quota | `alter user <user> quota unlimited on users` |
223
+ | Import row red: *incomplete host/database* | the DBeaver entry has neither fields nor a parsable URL | open the connection in DBeaver once, or create the source by hand |
@@ -0,0 +1,146 @@
1
+ # Reference
2
+
3
+ Wire-level details: HTTP routes, tool contracts, the files on disk, and the plugin
4
+ configuration. For usage see [guide.md](guide.md).
5
+
6
+ Everything below is served under `/plugins/db-mcp/api` on the DSH web origin. Reads
7
+ are `GET`; every mutation is `POST` (the same-origin check compares `Origin` with
8
+ `Host`). Responses are JSON with `cache-control: no-store`.
9
+
10
+ ## HTTP routes
11
+
12
+ | Route | Purpose |
13
+ | ----- | ------- |
14
+ | `GET /health` | plugin version, Host facts, Python discovery, runtime paths, registry state, MCP status, settings |
15
+ | `GET /datasources` | registry, credentials projected away |
16
+ | `GET /schema?datasource=…&table=…[&schema=…]` | tables, or the columns of one table |
17
+ | `GET /groups` | declared groups plus every group label in use |
18
+ | `GET /dbeaver/scan` | DBeaver connections; never includes passwords |
19
+ | `GET /mcp/status` | Python server phase, tool list, last error, stderr tail |
20
+ | `GET /settings` · `POST /settings` | global write switch and audit switch |
21
+ | `GET /audit?limit=N` | tail of the tool-call audit log (default 20, max 200) |
22
+ | `GET /python/env` · `GET /python/status` | interpreter, venv path, installed driver versions |
23
+ | `POST /python/setup` | create the venv and install `server/requirements.txt` |
24
+ | `POST /datasources/save` | create or update a source (`id` immutable) |
25
+ | `POST /datasources/delete` | remove a source and its stored secret |
26
+ | `POST /datasources/test` | real login probe |
27
+ | `POST /datasources/bulk` | `action: test \| readOnly \| group \| delete` over `ids` |
28
+ | `POST /groups/save` · `POST /groups/delete` | create, rename (`from`) or delete a group |
29
+ | `POST /dbeaver/import` | copy selected DBeaver connections into the registry |
30
+ | `POST /mcp/start` · `POST /mcp/restart` | start or restart the Python MCP server |
31
+ | `POST /mcp/call` | dev helper: call one MCP tool through the panel |
32
+ | `POST /reload` | dev helper: re-import `src/host.js` without restarting DSH |
33
+
34
+ ### Status codes
35
+
36
+ | Code | Meaning |
37
+ | ---- | ------- |
38
+ | 200 | success; bodies carry `ok: true` |
39
+ | 400 | validation error (`id must match …`, `oracle requires config.serviceName or config.sid`, `group name is required`, …) |
40
+ | 403 | `Origin` present and different from `Host` |
41
+ | 404 | unknown route, unknown datasource (`unknown datasources: …`), unknown group |
42
+ | 405 | method not GET/HEAD/POST |
43
+ | 500 | unexpected failure, including a Host module that failed to load |
44
+ | 503 | MCP server not ready (`start the MCP server from the panel and try again`) |
45
+
46
+ ## Tools
47
+
48
+ Names are prefixed at publication time: `mcp__db__list_datasources` and so on.
49
+ `datasource` is optional everywhere — omitted, the default source is used.
50
+ Row limits come from the server: `DB_MCP_MAX_ROWS` (default 200) per call, capped at
51
+ `DB_MCP_MAX_ROWS_HARD` (default 2000).
52
+
53
+ | Tool | Parameters | Returns |
54
+ | ---- | ---------- | ------- |
55
+ | `list_datasources` | — | `[{ id, name, kind, target, readOnly, isDefault }]` |
56
+ | `list_tables` | `datasource?`, `schema?` | `{ datasource, tables: [{ schema, name, type }] }` |
57
+ | `describe_table` | `table`, `datasource?`, `schema?` | `{ columns: [{ name, type, nullable, default, primaryKey }] }` |
58
+ | `sample_rows` | `table`, `datasource?`, `limit=20`, `schema?` | `{ columns, rows, rowCount, truncated }` |
59
+ | `run_query` | `sql`, `datasource?`, `max_rows?` | `{ columns, rows, rowCount, truncated, elapsedMs }` |
60
+ | `explain_query` | `sql`, `datasource?` | `{ plan }` (engine-specific text) |
61
+ | `get_ddl` | `table`, `datasource?`, `schema?` | `{ ddl, source: 'sqlite_master' \| 'columns' }` |
62
+ | `test_connection` | `datasource?` or `kind`+`config`+`password?`, `read_only?` | `{ ok, latencyMs, message, detail: { stage, server, database, user, schema } }` |
63
+
64
+ Failures come back as `{ ok: false, error: { code, message, hint? } }` with
65
+
66
+ | Code | Meaning |
67
+ | ---- | ------- |
68
+ | `user_error` | the request is wrong: unknown datasource, refused write, bad table name |
69
+ | `db_error` | the engine refused or failed: authentication, syntax, timeout |
70
+ | `infra_error` | the plugin could not get that far: interpreter missing, venv broken |
71
+
72
+ ## Files
73
+
74
+ All under `$DSH_HOME/db-mcp` (override with the `runtimeDir` option), mode `0600`,
75
+ written atomically via a temporary file plus rename.
76
+
77
+ `datasources.json`
78
+
79
+ ```json
80
+ {
81
+ "schemaVersion": 1,
82
+ "updatedAt": "2026-11-07T09:00:00.000Z",
83
+ "groups": ["docker"],
84
+ "datasources": [
85
+ {
86
+ "id": "pg-demo",
87
+ "name": "PostgreSQL demo",
88
+ "kind": "postgres",
89
+ "group": "docker",
90
+ "config": { "host": "127.0.0.1", "port": 5432, "database": "demo", "user": "demo", "schema": "public" },
91
+ "readOnly": true,
92
+ "isDefault": true,
93
+ "tags": [],
94
+ "createdAt": "…",
95
+ "updatedAt": "…",
96
+ "lastProbe": { "ok": true, "message": "PostgreSQL 15.19 · demo@demo · select version() ok", "latencyMs": 140, "at": "…" }
97
+ }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ `secrets.json` holds `{ "schemaVersion": 1, "secrets": { "<id>": { "password": "…" } } }`
103
+ and is read only by the Host, never sent to the page.
104
+
105
+ `settings.json` holds `{ "allowWrite": false, "auditLog": true, "updatedAt": "…" }`.
106
+ `allowWrite` counts only when it is exactly `true`.
107
+
108
+ `logs/audit.jsonl` — one JSON object per tool call; rotates to `audit.1.jsonl` at
109
+ 512 KiB:
110
+
111
+ ```json
112
+ {"at":"2026-11-08T06:20:28.000Z","tool":"mcp__db__run_query","datasource":"pg-demo","ok":true,"ms":41,"rows":3,"sql":"select id, name from customers order by id"}
113
+ ```
114
+
115
+ ### Data source fields
116
+
117
+ | Kind | `config` keys |
118
+ | ---- | ------------- |
119
+ | `sqlite` | `file` (absolute path), `readOnlyOpen?`, `busyTimeoutMs?` |
120
+ | `postgres` | `host`, `port` (5432), `database`, `user`, `schema?`, `sslMode?` |
121
+ | `mysql` | `host`, `port` (3306), `database`, `user`, `charset?` |
122
+ | `oracle` | `host`, `port` (1521), `serviceName` **or** `sid`, `user`, `schema?` |
123
+
124
+ `id` must match `^[a-z0-9][a-z0-9-]{0,31}$` and never changes; `name` ≤ 64
125
+ characters; `group` ≤ 48.
126
+
127
+ ## Plugin configuration
128
+
129
+ The bundle row in the profile accepts these options (all optional):
130
+
131
+ | Key | Default | Meaning |
132
+ | --- | ------- | ------- |
133
+ | `pythonPath` | auto | interpreter to use ahead of the venv and `PATH` |
134
+ | `runtimeDir` | `$DSH_HOME/db-mcp` | where the registry, secrets, settings and logs live |
135
+ | `probeTimeoutMs` | `10000` | per-probe timeout for *Test* |
136
+ | `mcpServerName` | `db` | prefix after `mcp__` when publishing tools |
137
+ | `debug` | `false` | log Host activity to the DSH console |
138
+
139
+ The Python side reads `DB_MCP_RUNTIME`, `DB_MCP_ALLOW_WRITE`, `DB_MCP_MAX_ROWS` and
140
+ `DB_MCP_MAX_ROWS_HARD`; the Host sets the first two when spawning.
141
+
142
+ ## Runtime requirements
143
+
144
+ - Node 22+ on the Host (the plugin uses `node:sqlite` for the SQLite probe).
145
+ - A Python 3.10+ interpreter for the MCP server, with the four packages from
146
+ `server/requirements.txt`. `mcp` must stay on 1.x — 2.x renamed `FastMCP`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-db-mcp",
3
- "version": "0.6.9",
4
- "description": "DSH database plugin: a data-source management panel plus a Python MCP server that exposes those sources to the agent.",
3
+ "version": "0.6.10",
4
+ "description": "DSH plugin: import the connections DBeaver already knows, then let the agent query SQLite, PostgreSQL, MySQL and Oracle through eight MCP tools. No DSH files to edit, no data source to retype.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
package/src/host.js CHANGED
@@ -33,7 +33,7 @@ const { createBridge } = await import(new URL(`./tools.js?v=${GENERATION}`, impo
33
33
 
34
34
  const API_PREFIX = '/plugins/db-mcp/api'
35
35
  const PLUGIN_NAME = 'dsh-db-mcp'
36
- const PLUGIN_VERSION = '0.6.9'
36
+ const PLUGIN_VERSION = '0.6.10'
37
37
  const SCHEMA_VERSION = 1
38
38
  const REGISTRY_FILE = 'datasources.json'
39
39
  const SECRETS_FILE = 'secrets.json'