dsh-db-mcp 0.6.4

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/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # dsh-db-mcp
2
+
3
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
4
+
5
+ DSH plugin that adds a **Database** entry to the sidebar with a data-source
6
+ management panel, backed by a Python MCP server that lets the agent query those
7
+ databases through tools (not through the page).
8
+
9
+ - One panel for every engine: SQLite, PostgreSQL, MySQL, Oracle.
10
+ - One connection string fills the whole form; passwords never leave the machine
11
+ and never come back to the page.
12
+ - Import the connections DBeaver already has, including its encrypted passwords.
13
+ - Read-only by default, with two independent write gates and an audit log.
14
+ - The agent gets seven MCP tools, not a SQL editor in the UI.
15
+
16
+ Requirements and staged plan: [`docs/requirements.md`](docs/requirements.md).
17
+
18
+ ## What exists today (P7)
19
+
20
+ - **Host half** (`index.js` 鈫?`src/host.js`): a Cordis plugin serving
21
+ `/plugins/db-mcp/api` from the DSH web server.
22
+ | Route | Purpose |
23
+ | ----- | ------- |
24
+ | `GET /health` | plugin version, Host facts, Python discovery, runtime paths, registry state, MCP status, settings |
25
+ | `GET /datasources` | registry, credentials projected away |
26
+ | `GET /schema?datasource=…&table=…[&schema=…]` | read-only structure: tables, or the columns of one table |
27
+ | `GET /dbeaver/scan` | connections already configured in DBeaver |
28
+ | `GET /mcp/status` | Python server phase, tool list, last error, stderr tail |
29
+ | `GET /settings` 路 `POST /settings` | global write switch and audit switch |
30
+ | `GET /audit?limit=N` | the tail of the tool-call audit log |
31
+ | `GET /python/env` | interpreter, venv path, installed driver versions |
32
+ | `POST /python/setup` | create the venv and `pip install -r server/requirements.txt` |
33
+ | `POST /datasources/save` | create or update a source (`id` is immutable) |
34
+ | `POST /datasources/delete` | remove a source and its stored secret |
35
+ | `POST /datasources/test` | real login probe: SQLite over `node:sqlite`, the rest through the Python drivers |
36
+ | `POST /dbeaver/import` | copy selected DBeaver connections into the registry |
37
+ | `POST /mcp/start` 路 `POST /mcp/restart` | start or restart the Python MCP server |
38
+ | `POST /mcp/call` | dev helper: call one MCP tool through the panel |
39
+ | `POST /reload` | dev helper: re-import `src/host.js` without restarting DSH |
40
+ - **Client half** (`client.js`): sidebar entry (`db-mcp`, order 25) plus the main
41
+ panel with the source list, the minimal create/edit form (one connection string),
42
+ test/delete/default actions, a read-only *structure* browser, the DBeaver import
43
+ flow, a *Restart MCP* button, the runtime diagnostics card (tool count, global
44
+ write switch, venv and package versions with a one-click install) and an
45
+ *audit log* card. Copy is localized (zh/en) through the client `locale` service.
46
+ - **Python server** (`server/`): `main.py` (FastMCP, seven tools), `engines.py`
47
+ (per-engine connect/introspect/query), `store.py` (reads the registry).
48
+ The plugin spawns it over stdio and publishes its tools as `mcp__db__*` on the
49
+ host `tools` service — it does not go through a `dsh-mcp-client` Loader row.
50
+
51
+ ### Tools the agent gets
52
+
53
+ | Tool | Purpose |
54
+ | ---- | ------- |
55
+ | `mcp__db__list_datasources` | the registered sources (id, kind, target, read-only) |
56
+ | `mcp__db__list_tables` | tables/views of one source |
57
+ | `mcp__db__describe_table` | columns, types, nullability, primary key |
58
+ | `mcp__db__sample_rows` | a few rows of one table |
59
+ | `mcp__db__run_query` | one statement; read-only sources refuse writes |
60
+ | `mcp__db__explain_query` | query plan |
61
+ | `mcp__db__get_ddl` | `CREATE TABLE` statement (SQLite: as created; others: rebuilt from the catalog) |
62
+ | `mcp__db__test_connection` | log in for real, run `select 1`, report server/user/schema |
63
+
64
+ `datasource` is optional everywhere: omitted, the default source is used.
65
+
66
+ ### Python environment
67
+
68
+ 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:
69
+
70
+ python -m venv C:\Users\<you>\.dsh\db-mcp\.venv
71
+ C:\Users\<you>\.dsh\db-mcp\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
72
+
73
+ `mcp` must stay on 1.x (2.x renamed `FastMCP` to `MCPServer`).
74
+
75
+ ## Does the Test button really log in?
76
+
77
+ Yes. For PostgreSQL, MySQL and Oracle the panel's **Test** action calls
78
+ `mcp__db__test_connection` over the Python MCP server, so the probe is a real
79
+ authenticated login plus `select 1` (Oracle uses `select 1 from dual`). The
80
+ panel row then shows the server banner, the connected user and database, and how
81
+ long the login took, for example:
82
+
83
+ ```
84
+ Oracle AI Database 26ai Free Release 23.26.3.0.0 路 DEMO@FREEPDB1 路 select 1 from dual ok
85
+ PostgreSQL 15.19 路 demo@demo 路 select version() ok
86
+ MySQL 8.0.46 路 demo@%@demo 路 select version() ok
87
+ ```
88
+
89
+ A wrong password or an unreachable listener now surfaces the driver's own
90
+ message (`ORA-01017`, `DPY-6005`, `password authentication failed` — instead of
91
+ "port reachable". Testing an unsaved form works too: `test_connection` accepts
92
+ `kind` + `config` + `password` without writing the registry first. When the
93
+ Python server cannot start, the probe degrades to a TCP check and says so.
94
+
95
+ ## Filling the form from one string
96
+
97
+ The create/edit form is deliberately as small as DBeaver's connection dialog:
98
+ **connection string**, **user**, **password** (with a *save password* box), and
99
+ that is it. Pasting a JDBC URL, a DSN, an Oracle TNS descriptor or a `.db` path
100
+ fills type, host, port, database/service, name and identifier; the derived values
101
+ only show up under **More settings**, together with schema, SID, SSL mode,
102
+ charset and the read-only switch.
103
+
104
+ Accepted shapes (see `dev/parse.test.mjs` for the full matrix):
105
+
106
+ ```
107
+ postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
108
+ jdbc:postgresql://localhost:5432/appdb
109
+ mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
110
+ jdbc:mariadb://10.0.0.5:3307/legacy
111
+ oracle://system:oracle@localhost:1521/XEPDB1
112
+ jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # service name
113
+ jdbc:oracle:thin:@ora:1521:ORCL # SID
114
+ (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
115
+ jdbc:sqlite:C:\data\app.db
116
+ sqlite:///C:/data/chinook.db
117
+ C:\data\app.db / /var/lib/app.sqlite3
118
+ ```
119
+
120
+ A password found inside the URL is sent once to `POST /datasources/save` and
121
+ then only stored in `secrets.json`; it is never echoed back.
122
+
123
+ Writes are `POST` only (the same-origin check compares `Origin` with `Host`).
124
+ Passwords never enter `datasources.json`; they live in `secrets.json` and are
125
+ never returned to the page (`hasSecret` is the only signal).
126
+
127
+ ## DBeaver import
128
+
129
+ `GET /dbeaver/scan` looks for `data-sources.json` under
130
+ `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\` (plus the `General` and
131
+ Linux/macOS layouts). When the sibling `credentials-config.json` exists it is
132
+ 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
133
+ imports the connection with an empty password, which the UI shows as
134
+ `no password`.
135
+
136
+ Connections created through the DBeaver driver dialog carry only a JDBC URL
137
+ (`configurationType: URL`) — no `database` field at all and no `user`. The
138
+ importer therefore parses `jdbc:mysql://host:port/db`,
139
+ `jdbc:postgresql://…` and `jdbc:oracle:thin:@…` for whatever the form fields
140
+ omit, and takes the user from the decrypted credentials when it is stored
141
+ there. The imported source is named after the DBeaver connection name (slugged
142
+ into an id), keeps `readOnly: true`, and gets the saved password copied into
143
+ `secrets.json`. Anything still missing is reported in `warnings`.
144
+
145
+ A connection that is already configured is not offered twice: every source
146
+ is identified by kind + host + port + database/service/SID + user (SQLite by
147
+ file path), and a DBeaver entry that matches one of them comes back with
148
+ `alreadyImported: true`, `duplicateId` and `duplicateName`. The panel disables
149
+ its checkbox and says *already added as …*; `POST /dbeaver/import` reports such
150
+ ids under `skipped` instead of creating a second copy.
151
+
152
+ The panel navigates with a tab bar (`数据源列表 / 新建数据源 / 从 DBeaver 导入`): the trailing step
153
+ is plain text, every earlier step is a link back, and no page needs a separate
154
+ "back to list" button.
155
+
156
+ ## Test databases in Docker
157
+
158
+ Nothing has to be installed on the host; every engine below is a throwaway
159
+ container with a demo account (`demo` / `dbmcp-demo`).
160
+
161
+ ```
162
+ # PostgreSQL — smallest and fastest to start, the recommended first target
163
+ docker run -d --name dbmcp-pg -p 5432:5432 \
164
+ -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
165
+ postgres:15-alpine
166
+
167
+ # MySQL
168
+ docker run -d --name dbmcp-mysql -p 3306:3306 \
169
+ -e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
170
+ -e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
171
+
172
+ # Oracle Free — several GB, a minute or two to become healthy
173
+ docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
174
+ gvenzl/oracle-free:23-slim
175
+ ```
176
+
177
+ On Windows, 1521 often falls inside a Hyper-V reserved port block and `docker run`
178
+ fails with *"An attempt was made to access a socket in a way forbidden by its
179
+ access permissions"*. Check `netsh interface ipv4 show excludedportrange
180
+ protocol=tcp` and publish another host port instead, e.g. `-p 11521:1521`; the
181
+ datasource then points at port 11521.
182
+
183
+ Then create a source in the panel with the matching connection string, e.g.
184
+ `postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo`,
185
+ `mysql://demo:dbmcp-demo@127.0.0.1:3306/demo` or
186
+ `oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1`.
187
+
188
+ ## Install into a profile
189
+
190
+ ```
191
+ plugin_manager install_bundle target=<absolute path to this directory>
192
+ ```
193
+
194
+ The bundle patch (`cordis.patch.yml`) inserts one Loader row with id `db-mcp`.
195
+ The profile links this directory, so file edits are live; the Host module is
196
+ imported once per process, hence `POST /plugins/db-mcp/api/reload` (or an app
197
+ restart) after changing `src/host.js`.
198
+
199
+ ## Panel layout
200
+
201
+ The page follows the same chrome as the other first-party panels:
202
+
203
+ - a masthead with **‹ 返回会话** (closes the panel through `ctx.layout.selectPanel(null)`),
204
+ the `数据库` title, and the connection/version pills;
205
+ - a tab row — **数据源列表 / 新建数据源 / 从 DBeaver 导入 / 运行时诊断 / 审计日志** — with
206
+ the active tab underlined;
207
+
208
+ Picking **新建数据源** or **从 DBeaver 导入** swaps only the card below the tabs — the
209
+ masthead and the tabs stay put. Editing reuses the **新建数据源** tab; the structure
210
+ browser is a sub-view of **数据源列表** with its own `← 数据源` link. There is no second
211
+ action bar: **刷新** and **重启 MCP** sit in the source-list card header, and the list
212
+ itself owns the group tools.
213
+
214
+ ## Schema overview
215
+ The **Structure** button on a source row opens a read-only browser: the tables
216
+ (or views) of that source on the left, and the columns of the selected table on
217
+ the right — name, type, nullability, default value and primary-key marker. It is
218
+ served by `GET /schema`, which only forwards `list_tables` / `describe_table` to
219
+ the MCP server, so the page itself can never run SQL. Nothing is cached: each
220
+ open reads the live catalog.
221
+
222
+ ## Groups and bulk actions
223
+
224
+ Every source carries an optional **group** label (`group` in the registry,
225
+ editable under *More settings*). Groups are user-owned: type a name in the
226
+ **新建分组** box to create one, rename or delete it from the group header, and drag
227
+ a source row onto a group to move it there (drag it onto **未分组** to clear the
228
+ label). The list renders one section per group, sorted by name with the ungrouped
229
+ sources last; while no group exists the list keeps its flat look.
230
+
231
+ Each group header carries a select-all box, so sources inside a group take part in
232
+ bulk work exactly like loose ones. Tick any row (or click its text) to open the
233
+ bulk bar: test all selected sources, flip them to read-only or writable, move them
234
+ into a group, or delete them (two-step confirmation). Bulk moves go through
235
+ `POST /datasources/bulk` with `{ action: 'test' | 'readOnly' | 'group' | 'delete', ids: [...], … }`,
236
+ while group CRUD uses `GET /groups`, `POST /groups/save` and `POST /groups/delete`.
237
+
238
+ ## Releasing
239
+
240
+ The package is published from GitHub Actions with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers):
241
+ `.github/workflows/publish.yml` trades the workflow's OIDC token for a short-lived
242
+ publish credential, so no `NPM_TOKEN` secret exists anywhere.
243
+
244
+ ```bash
245
+ node dev/bump.mjs 0.6.5 # package.json + src/host.js
246
+ git commit -am "chore: release 0.6.5"
247
+ git tag v0.6.5 && git push origin main --tags
248
+ ```
249
+
250
+ The workflow refuses to publish when the tag and `package.json` disagree, runs the
251
+ portable suites (`npm run test:ci`), publishes with `--provenance`, then waits for the
252
+ version to appear in the registry. Local `npm publish` still works for the very first
253
+ release, before a trusted publisher exists for the package.
254
+
255
+ ## Development
256
+
257
+ ```
258
+ node dev/selftest.mjs # mounts src/host.js on a throwaway port, 42 checks
259
+ node dev/shims.test.mjs # mounts index.js, checks routes, /reload, tool publication
260
+ node dev/parse.test.mjs # 24 checks: connection-string parser (DSN/JDBC/TNS/paths)
261
+ node dev/render.test.mjs # 37 checks: components render with a stub React
262
+ node dev/i18n.test.mjs # 12 checks: en/zh parity, no dead keys, no stray copy
263
+ node dev/p5.test.mjs # 15 checks: settings, audit log, python environment
264
+ node dev/mcp.test.mjs # 19 checks: real Python server over stdio, real SQLite + Oracle logins
265
+ ```
266
+
267
+ `npm test` runs everything except `mcp.test.mjs`, which needs the venv and the
268
+ Oracle container. `node dev/bump.mjs 0.6.2` updates both version strings.
269
+
270
+ ## Safety switches
271
+
272
+ Two independent gates decide whether a statement may write:
273
+
274
+ 1. **Global switch** — `allowWrite` in `<runtime>/settings.json`, toggled from the
275
+ diagnostics card. Off by default; while off every write statement is refused
276
+ with `user_error` and a hint pointing at the switch.
277
+ 2. **Per-source flag** — `readOnly` on the data source, on by default. A source
278
+ stays read-only even when the global switch is on until you clear its flag.
279
+
280
+ The Python server reads the global flag from `DB_MCP_ALLOW_WRITE` at start-up; the
281
+ host restarts it whenever the switch flips. Read-only is also enforced by the
282
+ driver itself where the engine allows it (`mode=ro` for SQLite,
283
+ `default_transaction_read_only` for PostgreSQL, `SET SESSION TRANSACTION READ ONLY`
284
+ for MySQL).
285
+
286
+ ## Audit log
287
+
288
+ Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
289
+ time, tool, source, duration, row count, the SQL truncated to 2000 characters and
290
+ the error when it failed. Result rows are never logged. The file rotates to
291
+ `audit.1.jsonl` at 512 KiB and logging can be turned off with `auditLog: false` in
292
+ `settings.json`. The panel shows the last dozen entries.
293
+
294
+ `dev/shims.test.mjs` also asserts that `package.json` has no UTF-8 BOM: the host
295
+ parses it with `JSON.parse`, which rejects a BOM, and the plugin then shows up as
296
+ `availability: missing` instead of loading. Editors that "add BOM" will break the
297
+ bundle silently — if the sidebar entry disappears after a restart, check this
298
+ first.
299
+
300
+ ## Stage map
301
+
302
+ | Stage | Content |
303
+ | ----- | ------- |
304
+ | P1 | installable skeleton: sidebar entry, empty panel, Host health API (done) |
305
+ | P2 | data-source CRUD, connection test, DBeaver import (done) |
306
+ | P3 | Python MCP server, tools published as `mcp__db__*` (done) |
307
+ | P4 | PostgreSQL / MySQL / Oracle dialects (done) |
308
+ | P5 | diagnostics, one-click venv, reload, audit log, global write switch (done) |
309
+ | P6 | i18n polish and docs (done) |
310
+ | P7 | read-only schema overview (done) |
311
+
312
+ ## License
313
+
314
+ Apache License 2.0 — see [LICENSE](LICENSE).
315
+
316
+ ## Runtime layout
317
+
318
+ ```
319
+ $DSH_HOME/db-mcp/
320
+ datasources.json # profiles, never passwords, 0600
321
+ secrets.json # per-source password, 0600, Host-only
322
+ settings.json # global write switch + audit switch, 0600
323
+ logs/mcp.log # masked log
324
+ logs/audit.jsonl # one line per MCP tool call, rotates at 512 KiB
325
+ .venv/ # plugin-local venv: mcp, psycopg, PyMySQL, oracledb
326
+ ```