dsh-db-mcp 0.6.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -347
- package/README.zh.md +42 -223
- package/docs/{requirements.md → design-notes.md} +5 -0
- package/docs/development.md +129 -0
- package/docs/guide.md +241 -0
- package/docs/reference.md +146 -0
- package/package.json +2 -2
- package/server/engines.py +2 -2
- package/server/main.py +13 -10
- package/src/host.js +1 -1
package/docs/guide.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
`dsh plugin` forwards its arguments to pnpm inside the profile directory, so it
|
|
10
|
+
needs `--profile` before the pnpm verb:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
dsh plugin --profile desktop add dsh-db-mcp # fresh install, desktop app
|
|
14
|
+
dsh plugin --profile web add dsh-db-mcp # the web UI's profile
|
|
15
|
+
dsh plugin --profile desktop add dsh-db-mcp@0.7.0 # pin a version
|
|
16
|
+
dsh plugin --profile desktop remove dsh-db-mcp # uninstall
|
|
17
|
+
dsh plugin --profile desktop why dsh-db-mcp # which version is installed
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`desktop` and `web` ship with DSH; any other name is initialized on first use
|
|
21
|
+
(`--profile scratch add dsh-db-mcp`). Check what a profile has with
|
|
22
|
+
`dsh plugin --profile desktop list dsh-db-mcp`.
|
|
23
|
+
|
|
24
|
+
From a source checkout, install the directory instead — the profile then links it,
|
|
25
|
+
so edits are live:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dsh plugin --profile desktop add C:\path\to\dsh-db-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Three things to know about upgrades:
|
|
32
|
+
|
|
33
|
+
- pnpm caches a package's metadata. A version published minutes ago may be
|
|
34
|
+
invisible to the dist-tag; naming the version sidesteps it.
|
|
35
|
+
- pnpm holds new releases behind a cooldown (`minimumReleaseAge`, 1440 minutes by
|
|
36
|
+
default) so a freshly published — possibly compromised — version is not resolved
|
|
37
|
+
automatically. Asking for an exact version makes pnpm record that version in
|
|
38
|
+
`minimumReleaseAgeExclude` inside the profile's `pnpm-workspace.yaml`; that is the
|
|
39
|
+
`Added 1 entry to minimumReleaseAgeExclude …` line you will see. Nothing is broken:
|
|
40
|
+
the exclusion is per version and only lets *that* pin through. To be asked instead,
|
|
41
|
+
set `minimumReleaseAgeStrict: true` in the profile's `pnpm-workspace.yaml`; to drop
|
|
42
|
+
stale entries, set `minimumReleaseAgeExcludePrune: true`.
|
|
43
|
+
- Upgrading needs either an exact version or a removed lock entry: `add` will not
|
|
44
|
+
move a dependency that still satisfies its existing range.
|
|
45
|
+
|
|
46
|
+
If the sidebar entry disappears after a restart, see [Troubleshooting](#troubleshooting).
|
|
47
|
+
|
|
48
|
+
## The panel
|
|
49
|
+
|
|
50
|
+
Sidebar → **Database**. The masthead carries **‹ Back to chat**, the title, and the
|
|
51
|
+
connection and version pills. Below it, five tabs:
|
|
52
|
+
|
|
53
|
+
| Tab | What it does |
|
|
54
|
+
| ---- | ------------ |
|
|
55
|
+
| Data source list | the registry: rows grouped by label, bulk actions, refresh, restart MCP |
|
|
56
|
+
| New data source | one connection string, user, password; also used for editing |
|
|
57
|
+
| Import from DBeaver | scan, tick, import |
|
|
58
|
+
| Runtime | Host facts, tool count, global write switch, venv and driver versions |
|
|
59
|
+
| Audit log | the last dozen tool calls, converted to your time zone |
|
|
60
|
+
|
|
61
|
+
**New data source** and **Import from DBeaver** swap only the card under the tabs —
|
|
62
|
+
the masthead and tabs stay put, and every earlier step is still a tab away.
|
|
63
|
+
|
|
64
|
+
## Adding a source
|
|
65
|
+
|
|
66
|
+
The form is as small as DBeaver's connection dialog: **connection string**, **user**,
|
|
67
|
+
**password** (with a *save password* box). Pasting fills type, host, port,
|
|
68
|
+
database/service, name and identifier; the derived values only appear under
|
|
69
|
+
**More settings**, together with schema, SID, SSL mode, charset and the read-only
|
|
70
|
+
switch.
|
|
71
|
+
|
|
72
|
+
Accepted shapes — a DSN, a JDBC URL, an Oracle TNS descriptor, a JDBC thin
|
|
73
|
+
descriptor, or a bare database file:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
|
|
77
|
+
jdbc:postgresql://localhost:5432/appdb
|
|
78
|
+
mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
|
|
79
|
+
jdbc:mariadb://10.0.0.5:3307/legacy
|
|
80
|
+
oracle://system:oracle@localhost:1521/XEPDB1
|
|
81
|
+
jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # service name
|
|
82
|
+
jdbc:oracle:thin:@ora:1521:ORCL # SID
|
|
83
|
+
(DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
|
|
84
|
+
jdbc:sqlite:C:\data\app.db
|
|
85
|
+
sqlite:///C:/data/chinook.db
|
|
86
|
+
C:\data\app.db / /var/lib/app.sqlite3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A password found inside the URL is sent once and then only stored; it is never
|
|
90
|
+
echoed back. `id` is immutable once created — rename the *display name* instead
|
|
91
|
+
(double-click the name on a row).
|
|
92
|
+
|
|
93
|
+
## Importing from DBeaver
|
|
94
|
+
|
|
95
|
+
The importer looks for `data-sources.json` under
|
|
96
|
+
`%APPDATA%\DBeaverData\workspace*\General\.dbeaver\`, plus the `General` and
|
|
97
|
+
Linux/macOS layouts. When the sibling `credentials-config.json` exists it is
|
|
98
|
+
decrypted with DBeaver's built-in key (AES-128-CBC, zero IV, 16-byte prefix — see
|
|
99
|
+
[Tatsh/dbeaver-creds](https://github.com/Tatsh/dbeaver-creds)). Ticked entries are
|
|
100
|
+
copied into the registry, their passwords into `secrets.json`.
|
|
101
|
+
|
|
102
|
+
Two details make real DBeaver workspaces work:
|
|
103
|
+
|
|
104
|
+
- Connections created through the driver dialog often carry only a JDBC URL
|
|
105
|
+
(`configurationType: URL`) with no `database` field and no `user`. The importer
|
|
106
|
+
parses the URL for whatever the form fields omit and takes the user from the
|
|
107
|
+
decrypted credentials.
|
|
108
|
+
- A connection already configured is not offered twice. Sources are identified by
|
|
109
|
+
kind + host + port + database/service/SID + user (SQLite by file path); a match
|
|
110
|
+
comes back as `alreadyImported`, the checkbox is disabled and the row says
|
|
111
|
+
*already added as …*.
|
|
112
|
+
|
|
113
|
+
A workspace that never saved passwords is fine: the connection is imported with an
|
|
114
|
+
empty password and the row shows **no password**. Fill it in once and it stays.
|
|
115
|
+
|
|
116
|
+
## Testing a connection
|
|
117
|
+
|
|
118
|
+
**Test** performs a real login: SQLite is opened read-only through `node:sqlite`,
|
|
119
|
+
the other engines go through the Python drivers, which authenticate and run
|
|
120
|
+
`select 1` (Oracle: `select 1 from dual`). The row then shows the banner, the user
|
|
121
|
+
and database, and how long the login took:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok
|
|
125
|
+
PostgreSQL 15.19 · demo@demo · select version() ok
|
|
126
|
+
MySQL 8.0.46 · demo@%@demo · select version() ok
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Wrong passwords now surface the driver's own message (`ORA-01017`, `DPY-6005`,
|
|
130
|
+
`password authentication failed`) instead of "port reachable". An unsaved form can
|
|
131
|
+
be tested too. When the Python server cannot start, the probe degrades to a TCP
|
|
132
|
+
check and says so.
|
|
133
|
+
|
|
134
|
+
## Groups and bulk actions
|
|
135
|
+
|
|
136
|
+
Every source carries an optional **group** label, editable under *More settings*.
|
|
137
|
+
|
|
138
|
+
- **Create** — type a name in **New group**.
|
|
139
|
+
- **Rename** — double-click the group name; **delete** drops the label from its
|
|
140
|
+
members.
|
|
141
|
+
- **Move** — drag a source row onto a group, or onto **Ungrouped** to clear it.
|
|
142
|
+
- **Rename a source** — double-click its name; the id never changes.
|
|
143
|
+
- **Select** — each group header has a select-all box, so grouped sources join bulk
|
|
144
|
+
work exactly like loose ones.
|
|
145
|
+
|
|
146
|
+
With a selection made, the bulk bar can test, flip read-only, move into a group, or
|
|
147
|
+
delete (two-step confirmation).
|
|
148
|
+
|
|
149
|
+
## Structure browser
|
|
150
|
+
|
|
151
|
+
**Structure** on a source row opens a read-only view: tables and views on the left,
|
|
152
|
+
columns of the selected table on the right (name, type, nullability, default,
|
|
153
|
+
primary-key marker). It is served by `GET /schema`, which only forwards
|
|
154
|
+
`list_tables` / `describe_table` to the MCP server — the page cannot run SQL, and
|
|
155
|
+
nothing is cached.
|
|
156
|
+
|
|
157
|
+
## Safety switches
|
|
158
|
+
|
|
159
|
+
Two independent gates decide whether a statement may write:
|
|
160
|
+
|
|
161
|
+
1. **Global switch** — `allowWrite` in `<runtime>/settings.json`, toggled on the
|
|
162
|
+
*Runtime* tab. Off by default; while off every write is refused with
|
|
163
|
+
`user_error` and a hint pointing at the switch.
|
|
164
|
+
2. **Per-source flag** — `readOnly` on the source, on by default. Click the
|
|
165
|
+
*read-only* / *writable* pill on a row to flip it in one click; the pill turns
|
|
166
|
+
amber while that source accepts writes.
|
|
167
|
+
|
|
168
|
+
The Python server reads the global flag at start-up and the host restarts it
|
|
169
|
+
whenever the switch flips. Read-only is enforced again at the driver where the
|
|
170
|
+
engine allows it (`mode=ro` for SQLite, `default_transaction_read_only` for
|
|
171
|
+
PostgreSQL, `SET SESSION TRANSACTION READ ONLY` for MySQL).
|
|
172
|
+
|
|
173
|
+
## Audit log
|
|
174
|
+
|
|
175
|
+
Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
|
|
176
|
+
time, tool, source, duration, row count, the SQL truncated to 2000 characters, and
|
|
177
|
+
the error when it failed. Result rows are never logged. The file rotates to
|
|
178
|
+
`audit.1.jsonl` at 512 KiB and logging can be switched off with `auditLog: false`.
|
|
179
|
+
|
|
180
|
+
The panel shows the last dozen entries **on your own clock**; the file keeps UTC so
|
|
181
|
+
it stays comparable across machines. Each stamp carries the full local timestamp in
|
|
182
|
+
its tooltip.
|
|
183
|
+
|
|
184
|
+
## Runtime layout
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
$DSH_HOME/db-mcp/
|
|
188
|
+
datasources.json # sources and groups, never passwords, 0600
|
|
189
|
+
secrets.json # per-source password, 0600, Host-only
|
|
190
|
+
settings.json # global write switch + audit switch, 0600
|
|
191
|
+
logs/mcp.log # masked log
|
|
192
|
+
logs/audit.jsonl # one line per MCP tool call, rotates at 512 KiB
|
|
193
|
+
.venv/ # plugin-local venv: mcp, psycopg, PyMySQL, oracledb
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Test databases in Docker
|
|
197
|
+
|
|
198
|
+
Nothing has to be installed on the host; each engine below is a throwaway
|
|
199
|
+
container with a demo account (`demo` / `dbmcp-demo`).
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
# PostgreSQL — smallest and fastest, the recommended first target
|
|
203
|
+
docker run -d --name dbmcp-pg -p 5432:5432 \
|
|
204
|
+
-e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
|
|
205
|
+
postgres:15-alpine
|
|
206
|
+
|
|
207
|
+
# MySQL
|
|
208
|
+
docker run -d --name dbmcp-mysql -p 3306:3306 \
|
|
209
|
+
-e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
|
|
210
|
+
-e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
|
|
211
|
+
|
|
212
|
+
# Oracle Free — several GB, a minute or two to become healthy
|
|
213
|
+
docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
|
|
214
|
+
gvenzl/oracle-free:23-slim
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
On Windows, 1521 often falls inside a Hyper-V reserved port block and `docker run`
|
|
218
|
+
fails with *"An attempt was made to access a socket in a way forbidden by its access
|
|
219
|
+
permissions"*. Check `netsh interface ipv4 show excludedportrange protocol=tcp` and
|
|
220
|
+
publish another host port, e.g. `-p 11521:1521`; point the datasource at 11521.
|
|
221
|
+
|
|
222
|
+
Then import or create a source with the matching string:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo
|
|
226
|
+
mysql://demo:dbmcp-demo@127.0.0.1:3306/demo
|
|
227
|
+
oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
| Symptom | Cause | Fix |
|
|
233
|
+
| ------- | ----- | --- |
|
|
234
|
+
| 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 (any editor's "UTF-8 without BOM"), then toggle the plugin off and on in the **Plugins** panel |
|
|
235
|
+
| 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 |
|
|
236
|
+
| 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` |
|
|
237
|
+
| Tools missing from the agent | the Python server never started | *Runtime* → tools count and error; then **Restart MCP** |
|
|
238
|
+
| `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** |
|
|
239
|
+
| Oracle login fails with `DPY-6005` | listener unreachable, or host/port wrong | `docker ps`, then *Test* again |
|
|
240
|
+
| `ORA-01950` on first write | Oracle user has no tablespace quota | `alter user <user> quota unlimited on users` |
|
|
241
|
+
| 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
|
+
| `execute_sql` | `sql`, `datasource?`, `max_rows?` | `{ columns, rows, rowCount, truncated, elapsedMs }`, or `{ affectedRows }` for a write |
|
|
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__execute_sql","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.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "0.7.0",
|
|
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/server/engines.py
CHANGED
|
@@ -549,7 +549,7 @@ def table_ddl(profile: dict, connection, table: str, schema: str | None = None)
|
|
|
549
549
|
# queries
|
|
550
550
|
|
|
551
551
|
|
|
552
|
-
def
|
|
552
|
+
def execute_sql(profile: dict, connection, sql: str, max_rows: int) -> dict:
|
|
553
553
|
ensure_readable(sql, profile)
|
|
554
554
|
kind = profile.get("kind")
|
|
555
555
|
started = time.perf_counter()
|
|
@@ -613,7 +613,7 @@ def connection_autocommit(kind: str) -> bool:
|
|
|
613
613
|
|
|
614
614
|
def sample_rows(profile: dict, connection, table: str, limit: int, schema: str | None) -> dict:
|
|
615
615
|
sql = f"SELECT * FROM {qualified(profile, table)}{_limit_clause(profile.get('kind'), limit)}"
|
|
616
|
-
result =
|
|
616
|
+
result = execute_sql(profile, connection, sql, limit)
|
|
617
617
|
result["table"] = table
|
|
618
618
|
result["schema"] = schema or (profile.get("config") or {}).get("schema") or None
|
|
619
619
|
return result
|
package/server/main.py
CHANGED
|
@@ -27,9 +27,11 @@ HARD_MAX_ROWS = int(os.environ.get("DB_MCP_MAX_ROWS_HARD") or 2000)
|
|
|
27
27
|
mcp = FastMCP(
|
|
28
28
|
"db-mcp",
|
|
29
29
|
instructions=(
|
|
30
|
-
"
|
|
30
|
+
"Work with the databases the user registered in the DSH Database panel. "
|
|
31
31
|
"Start with list_datasources, then list_tables, describe_table and finally "
|
|
32
|
-
"
|
|
32
|
+
"execute_sql. execute_sql runs any single statement: SELECT and EXPLAIN when "
|
|
33
|
+
"the source is read-only, INSERT/UPDATE/DELETE/DDL only when writes are "
|
|
34
|
+
"enabled. Prefer these tools over guessing table names."
|
|
33
35
|
),
|
|
34
36
|
)
|
|
35
37
|
|
|
@@ -201,19 +203,20 @@ def sample_rows(
|
|
|
201
203
|
|
|
202
204
|
|
|
203
205
|
@mcp.tool()
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
Read-only datasources accept SELECT/WITH/SHOW/EXPLAIN statements only
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`affectedRows` instead of a
|
|
206
|
+
def execute_sql(sql: str, datasource: Optional[str] = None, max_rows: Optional[int] = None) -> dict:
|
|
207
|
+
"""Execute one SQL statement — a query or, when writes are enabled, a change.
|
|
208
|
+
|
|
209
|
+
Read-only datasources accept SELECT/WITH/SHOW/EXPLAIN statements only; every
|
|
210
|
+
other statement is refused before it reaches the database. Results are capped by
|
|
211
|
+
`max_rows` (default 200) and `truncated` says whether more rows existed. Writes
|
|
212
|
+
(INSERT/UPDATE/DELETE/DDL) are committed and report `affectedRows` instead of a
|
|
213
|
+
result set. One statement per call: no scripts, no `;`-separated batches.
|
|
211
214
|
"""
|
|
212
215
|
try:
|
|
213
216
|
limit = _clamp_rows(max_rows)
|
|
214
217
|
profile, (connection, close) = _open(datasource)
|
|
215
218
|
try:
|
|
216
|
-
result = engines.
|
|
219
|
+
result = engines.execute_sql(profile, connection, sql, limit)
|
|
217
220
|
finally:
|
|
218
221
|
close()
|
|
219
222
|
return {"ok": True, "datasource": _source(profile), **result}
|
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.
|
|
36
|
+
const PLUGIN_VERSION = '0.7.0'
|
|
37
37
|
const SCHEMA_VERSION = 1
|
|
38
38
|
const REGISTRY_FILE = 'datasources.json'
|
|
39
39
|
const SECRETS_FILE = 'secrets.json'
|