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/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.6.9",
4
- "description": "DSH database plugin: a data-source management panel plus a Python MCP server that exposes those sources to the agent.",
3
+ "version": "0.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 run_query(profile: dict, connection, sql: str, max_rows: int) -> dict:
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 = run_query(profile, connection, sql, limit)
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
- "Query the databases the user registered in the DSH Database panel. "
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
- "run_query. Prefer these tools over guessing table names."
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 run_query(sql: str, datasource: Optional[str] = None, max_rows: Optional[int] = None) -> dict:
205
- """Run one SQL statement and return columns plus rows.
206
-
207
- Read-only datasources accept SELECT/WITH/SHOW/EXPLAIN statements only.
208
- Results are capped by `max_rows` (default 200); `truncated` says whether more
209
- rows existed. Writes (INSERT/UPDATE/DELETE/DDL) are committed and report
210
- `affectedRows` instead of a result set.
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.run_query(profile, connection, sql, limit)
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.6.9'
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'