dsh-db-mcp 0.6.8 → 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 +64 -332
- package/README.zh.md +38 -215
- package/docs/{requirements.md → design-notes.md} +5 -0
- package/docs/development.md +99 -0
- package/docs/guide.md +223 -0
- package/docs/reference.md +146 -0
- package/package.json +2 -2
- package/src/host.js +1 -1
- package/src/mcp.js +45 -1
package/README.md
CHANGED
|
@@ -1,355 +1,87 @@
|
|
|
1
1
|
# dsh-db-mcp
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-db-mcp)
|
|
4
5
|
|
|
5
6
|
English | [中文](README.zh.md)
|
|
6
7
|
|
|
7
|
-
DSH plugin that
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
+
## Zero configuration, in three senses
|
|
19
16
|
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
`/plugins/db-mcp/api` from the DSH web server.
|
|
28
|
+
## Run
|
|
24
29
|
|
|
25
|
-
|
|
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
|
-
|
|
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:
|
|
34
|
+
Then:
|
|
215
35
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- a tab row — **Data source list / New data source / Import from DBeaver /
|
|
220
|
-
Runtime / Audit log** — with the active tab underlined.
|
|
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?"*
|
|
221
39
|
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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.
|
|
228
42
|
|
|
229
|
-
|
|
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.
|
|
230
46
|
|
|
231
|
-
|
|
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.
|
|
47
|
+
## What the agent gets
|
|
237
48
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
## Audit log
|
|
314
|
-
|
|
315
|
-
Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
|
|
316
|
-
time, tool, source, duration, row count, the SQL truncated to 2000 characters and
|
|
317
|
-
the error when it failed. Result rows are never logged. The file rotates to
|
|
318
|
-
`audit.1.jsonl` at 512 KiB and logging can be turned off with `auditLog: false` in
|
|
319
|
-
`settings.json`. The panel shows the last dozen entries on **your own clock** —
|
|
320
|
-
the file keeps UTC so it stays comparable across machines, the card converts each
|
|
321
|
-
stamp to the viewer's time zone and prints the offset it used.
|
|
322
|
-
|
|
323
|
-
`dev/shims.test.mjs` also asserts that `package.json` has no UTF-8 BOM: the host
|
|
324
|
-
parses it with `JSON.parse`, which rejects a BOM, and the plugin then shows up as
|
|
325
|
-
`availability: missing` instead of loading. Editors that "add BOM" will break the
|
|
326
|
-
bundle silently — if the sidebar entry disappears after a restart, check this
|
|
327
|
-
first.
|
|
328
|
-
|
|
329
|
-
## Stage map
|
|
330
|
-
|
|
331
|
-
| Stage | Content |
|
|
332
|
-
| ----- | ------- |
|
|
333
|
-
| P1 | installable skeleton: sidebar entry, empty panel, Host health API (done) |
|
|
334
|
-
| P2 | data-source CRUD, connection test, DBeaver import (done) |
|
|
335
|
-
| P3 | Python MCP server, tools published as `mcp__db__*` (done) |
|
|
336
|
-
| P4 | PostgreSQL / MySQL / Oracle dialects (done) |
|
|
337
|
-
| P5 | diagnostics, one-click venv, reload, audit log, global write switch (done) |
|
|
338
|
-
| P6 | i18n polish and docs (done) |
|
|
339
|
-
| 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 |
|
|
340
84
|
|
|
341
85
|
## License
|
|
342
86
|
|
|
343
87
|
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
344
|
-
|
|
345
|
-
## Runtime layout
|
|
346
|
-
|
|
347
|
-
```
|
|
348
|
-
$DSH_HOME/db-mcp/
|
|
349
|
-
datasources.json # profiles, never passwords, 0600
|
|
350
|
-
secrets.json # per-source password, 0600, Host-only
|
|
351
|
-
settings.json # global write switch + audit switch, 0600
|
|
352
|
-
logs/mcp.log # masked log
|
|
353
|
-
logs/audit.jsonl # one line per MCP tool call, rotates at 512 KiB
|
|
354
|
-
.venv/ # plugin-local venv: mcp, psycopg, PyMySQL, oracledb
|
|
355
|
-
```
|
package/README.zh.md
CHANGED
|
@@ -1,244 +1,67 @@
|
|
|
1
1
|
# dsh-db-mcp
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-db-mcp)
|
|
4
5
|
|
|
5
6
|
[English](README.md) | 中文
|
|
6
7
|
|
|
7
|
-
一个 DSH
|
|
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`):
|
|
8
|
+
一个 [DSH](https://github.com/deepseek-ai/deepseek-harness) 插件,把数据库交给智能体:侧栏多出一个**数据库**面板,登记过的每个数据源都会变成 `mcp__db__*` 工具。
|
|
87
9
|
|
|
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
|
-
```
|
|
10
|
+
**零配置。** 不需要改 DSH 里的任何文件,也不用手工抄一遍连接信息——直接把 DBeaver 里已有的连接导进来,然后开口问就行。
|
|
135
11
|
|
|
136
|
-
|
|
12
|
+
## 三个方面都不用配
|
|
137
13
|
|
|
138
|
-
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
`oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1`。
|
|
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、装四个驱动包,入口在 **运行时诊断 → 安装 / 修复依赖**。
|
|
142
17
|
|
|
143
|
-
##
|
|
18
|
+
## 运行
|
|
144
19
|
|
|
145
|
-
```
|
|
146
|
-
plugin_manager install_bundle target=<本目录的绝对路径>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
或者从 npm 装:
|
|
150
|
-
|
|
151
|
-
```
|
|
20
|
+
```sh
|
|
152
21
|
dsh plugin add dsh-db-mcp@latest
|
|
153
22
|
```
|
|
154
23
|
|
|
155
|
-
|
|
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
|
-
```
|
|
24
|
+
然后:
|
|
200
25
|
|
|
201
|
-
|
|
26
|
+
1. 侧栏 → **数据库**。
|
|
27
|
+
2. **从 DBeaver 导入** → 勾选连接 → **导入所选**。
|
|
28
|
+
3. 问智能体一句:*「我的 Chinook 库里有哪些表?」*
|
|
202
29
|
|
|
203
|
-
|
|
30
|
+
如果某条连接在 DBeaver 里没保存口令,卡片会显示**无密码**;打开该数据源补一次即可——口令只写进 `secrets.json`,不会再回到页面上。
|
|
204
31
|
|
|
205
|
-
|
|
32
|
+
想从零开始也行:**新建数据源**里粘一条连接串(`postgresql://user:pw@host:5432/db`、JDBC URL、Oracle TNS 描述符或 `.db` 路径),表单会自动填好。
|
|
206
33
|
|
|
207
|
-
|
|
208
|
-
2. **单源标记** —— 数据源的 `readOnly`,默认开启。即使全局开关打开,只要不清掉这条标记,该源仍然只读。点数据源行上的「只读 / 可写」药丸即可一键切换:该源可写时药丸变成琥珀色,若全局开关仍关着,提示条会提醒你去打开它。
|
|
34
|
+
## 智能体拿到的工具
|
|
209
35
|
|
|
210
|
-
|
|
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 |
|
|
211
46
|
|
|
212
|
-
|
|
47
|
+
`datasource` 在所有工具里都是可选的,省略就用默认数据源。查询发生在**对话里**,面板只负责管理连接,不是 SQL 控制台。
|
|
213
48
|
|
|
214
|
-
|
|
49
|
+
## 安全
|
|
215
50
|
|
|
216
|
-
|
|
51
|
+
- **默认只读**,两道互相独立的闸门:全局写开关 + 单源标记,两者都打开才允许写入。
|
|
52
|
+
- **口令不出本机。** 存在 `secrets.json`(`0600`);任何 HTTP 接口都不返回它,Python 子进程拿到的也只是很短的环境变量白名单,而不是整个 DSH 环境。
|
|
53
|
+
- **每次工具调用都有审计**,写入 `logs/audit.jsonl`——工具、数据源、耗时、行数、SQL;结果行永不记录。
|
|
54
|
+
- **面板不会执行 SQL。** 结构浏览只转发 `list_tables` 与 `describe_table`。
|
|
217
55
|
|
|
218
|
-
##
|
|
56
|
+
## 文档
|
|
219
57
|
|
|
220
|
-
|
|
|
58
|
+
| 文档 | 内容 |
|
|
221
59
|
| ---- | ---- |
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
| P5 | 诊断、一键 venv、热重载、审计日志、全局写开关(已完成) |
|
|
227
|
-
| P6 | 多语言打磨与文档(已完成) |
|
|
228
|
-
| 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)与决策记录(英文) |
|
|
229
64
|
|
|
230
65
|
## 许可
|
|
231
66
|
|
|
232
67
|
Apache License 2.0 —— 见 [LICENSE](LICENSE)。
|
|
233
|
-
|
|
234
|
-
## 运行时目录
|
|
235
|
-
|
|
236
|
-
```
|
|
237
|
-
$DSH_HOME/db-mcp/
|
|
238
|
-
datasources.json # 数据源档案,不含口令,0600
|
|
239
|
-
secrets.json # 各源口令,0600,仅 Host 可读
|
|
240
|
-
settings.json # 全局写开关 + 审计开关,0600
|
|
241
|
-
logs/mcp.log # 已掩码的日志
|
|
242
|
-
logs/audit.jsonl # 每次 MCP 工具调用一行,超过 512 KiB 轮转
|
|
243
|
-
.venv/ # 插件内 venv:mcp、psycopg、PyMySQL、oracledb
|
|
244
|
-
```
|
|
@@ -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.
|
|
4
|
-
"description": "DSH
|
|
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.
|
|
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'
|
package/src/mcp.js
CHANGED
|
@@ -16,6 +16,50 @@ import { spawn } from 'node:child_process';
|
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
17
|
const INIT_TIMEOUT_MS = 60_000;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The Python child gets a small, explicit environment instead of a copy of ours.
|
|
21
|
+
* Spreading `process.env` would hand every token and cloud credential in the
|
|
22
|
+
* DSH process to a driver stack that only ever needs to find its interpreter.
|
|
23
|
+
*/
|
|
24
|
+
const ENV_ALLOW = [
|
|
25
|
+
'PATH',
|
|
26
|
+
'Path',
|
|
27
|
+
'PATHEXT',
|
|
28
|
+
'SystemRoot',
|
|
29
|
+
'SYSTEMROOT',
|
|
30
|
+
'windir',
|
|
31
|
+
'ComSpec',
|
|
32
|
+
'COMSPEC',
|
|
33
|
+
'TEMP',
|
|
34
|
+
'TMP',
|
|
35
|
+
'TMPDIR',
|
|
36
|
+
'HOME',
|
|
37
|
+
'USERPROFILE',
|
|
38
|
+
'HOMEDRIVE',
|
|
39
|
+
'HOMEPATH',
|
|
40
|
+
'APPDATA',
|
|
41
|
+
'LOCALAPPDATA',
|
|
42
|
+
'PROGRAMDATA',
|
|
43
|
+
'PROGRAMFILES',
|
|
44
|
+
'LANG',
|
|
45
|
+
'LC_ALL',
|
|
46
|
+
'TZ',
|
|
47
|
+
'PWD',
|
|
48
|
+
'PYTHONPATH',
|
|
49
|
+
'PYTHONHOME',
|
|
50
|
+
'PYTHONIOENCODING',
|
|
51
|
+
'VIRTUAL_ENV',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function childEnvironment(extra) {
|
|
55
|
+
const env = {};
|
|
56
|
+
for (const key of ENV_ALLOW) {
|
|
57
|
+
const value = process.env[key];
|
|
58
|
+
if (typeof value === 'string' && value !== '') env[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return Object.assign(env, extra || {});
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
export class McpError extends Error {
|
|
20
64
|
constructor(message, { code = 'mcp_error', data } = {}) {
|
|
21
65
|
super(message);
|
|
@@ -61,7 +105,7 @@ export class McpClient {
|
|
|
61
105
|
if (this.#child) return this;
|
|
62
106
|
const child = spawn(this.command, this.args, {
|
|
63
107
|
cwd: this.cwd,
|
|
64
|
-
env:
|
|
108
|
+
env: childEnvironment(this.env),
|
|
65
109
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
110
|
windowsHide: true,
|
|
67
111
|
});
|