English | [中文](guide.zh.md) # Guide Everything the panel can do, in the order you meet it. The rest is on its own page: - [safety.md](safety.md) — what may write, where, and which of those rules follow you out of DSH - [clients.md](clients.md) — the HTTP endpoint, the tools list, running the server yourself - [runtime.md](runtime.md) — the files on disk, the Python dependencies, Docker test databases, troubleshooting - [reference.md](reference.md) — the wire protocol and the file formats - [development.md](development.md) — build, test and release ## The whole path, on one page You set connections up on the panel side (left); the conversation does the querying (right). The only hand-off is a data source that exists. ![The path from a DBeaver connection to an answer in chat](../asset/usage-guide.en.png) ## Install and upgrade `dsh plugin` forwards its arguments to pnpm inside the profile directory, so it needs `--profile` before the pnpm verb: ```sh dsh plugin --profile desktop add dsh-db-mcp # fresh install, desktop app dsh plugin --profile web add dsh-db-mcp # the web UI's profile dsh plugin --profile desktop add dsh-db-mcp@0.8.0 # pin a version dsh plugin --profile desktop remove dsh-db-mcp # uninstall dsh plugin --profile desktop why dsh-db-mcp # which version is installed ``` `desktop` and `web` ship with DSH; any other name is initialized on first use (`--profile scratch add dsh-db-mcp`). Check what a profile has with `dsh plugin --profile desktop list dsh-db-mcp`. From a source checkout, install the directory instead — the profile then links it, so edits are live: ```sh dsh plugin --profile desktop add C:\path\to\dsh-db-mcp ``` Three things to know about upgrades: - pnpm caches a package's metadata. A version published minutes ago may be invisible to the dist-tag; naming the version sidesteps it. - pnpm holds new releases behind a cooldown (`minimumReleaseAge`, 1440 minutes by default) so a freshly published — possibly compromised — version is not resolved automatically. Asking for an exact version makes pnpm record that version in `minimumReleaseAgeExclude` inside the profile's `pnpm-workspace.yaml`; that is the `Added 1 entry to minimumReleaseAgeExclude …` line you will see. Nothing is broken: the exclusion is per version and only lets *that* pin through. To be asked instead, set `minimumReleaseAgeStrict: true` in the profile's `pnpm-workspace.yaml`; to drop stale entries, set `minimumReleaseAgeExcludePrune: true`. - Upgrading needs either an exact version or a removed lock entry: `add` will not move a dependency that still satisfies its existing range. ### Updating from the app Settings → Plugins upgrades the plugin in place. Two things are worth knowing: - The server half is imported once at boot, so the new code only runs after you restart DSH. The panel picks the update up immediately; the MCP tools do not. - If the dialog stops at *stop and remove <old version>* with **other configurations still use components from this bundle**, the app that is running counts as one of those configurations. Restart DSH and update again — in practice that is all it takes. - On Windows the restart also matters for a second reason: a running DSH holds the package's files open, so the in-place swap would fail with `EPERM … rename`. If a restart is not an option, or several profiles kept their own copy, quit DSH completely and update every profile that already contains the plugin: ```powershell $profiles = 'desktop', 'web' foreach ($p in $profiles) { $file = "$env:USERPROFILE\.dsh\profiles\$p\package.json" if ((Test-Path $file) -and ((Get-Content $file -Raw) -match '"dsh-db-mcp"')) { dsh plugin --profile $p add dsh-db-mcp } } ``` If pnpm answers `Already up to date`, name the version to force the move: `dsh plugin --profile desktop add dsh-db-mcp@0.9.7`. Nothing you configured is at risk: sources, saved passwords, settings and the audit log live in `~/.dsh/db-mcp`, outside the package. If the sidebar entry disappears after a restart, see [Troubleshooting](runtime.md#troubleshooting). ## The panel Sidebar → **Data sources**. The masthead carries **‹ Back to chat**, the title, the connection and version pills, and a **Star** link to the [repository](https://github.com/halcyon6669/dsh-db-mcp) — the docs, the issues and the source all live there. Below the masthead, seven tabs: | Tab | What it does | | ---- | ------------ | | Data source list | the registry: health strip, search, group chips, rows grouped by label, bulk actions, refresh | | New data source | one connection string, user, password; also used for editing | | Import from DBeaver | scan, tick, import | | Connect | the HTTP endpoint and the outside agents it can be written into | | Runtime | Host facts, tool count, global write switch, venv and driver versions | | Tools | every tool the Python server publishes, with its on/off switch | | Audit log | the last dozen tool calls, converted to your time zone | **New data source** and **Import from DBeaver** swap only the card under the tabs — the masthead and tabs stay put, and every earlier step is still a tab away. The list opens with a status strip: whether the MCP bridge is up, the total number of sources, how many probed healthy, and which ones failed (with their names), with the search box on the right of that first row. Filter chips — **All**, one chip per group, **Ungrouped** and **Only failures** — take the second row. The box matches the name, identifier, host, kind and the last probe message; chips and the box narrow the list together, and a failed source keeps its red row and an expandable **Error details** block either way. Under each source's connection target sits its identifier as a chip labelled **ID**: that is the value every MCP tool takes as its `datasource` argument, so it is shown as a handle of its own rather than buried at the end of the target line. Click the chip to copy the bare identifier and paste it into chat ("query the `chinook` source"), or hover it to see the full value when it is too long for the row. ## Adding a source The form is as small as DBeaver's connection dialog: **connection string**, **user**, **password** (with a *save password* box). The string field is marked *smart parsing* and fills type, host, port, database/service, name and identifier as you paste. The derived values only appear under **More settings** — name, group and identifier in one row, then the engine, the connection fields and the read-only switch. The connection string, the user and the password are required for every engine but SQLite (which needs only the file): saving with one of them empty stops at the form, marks the empty field in red and leaves your typing alone. **Test** sits next to **Save** and runs the login without saving anything, reporting the result above the buttons. Editing an existing source may leave the password empty — that keeps the stored one. One endpoint may exist only once: the same host, port, database and user (SQLite: the same file) cannot be added twice. A second attempt comes back as `409` and names the row that already holds it, so the agent never sees two indistinguishable sources. The import screen enforces the same rule. The footer repeats where a saved source goes: it is live in the panel at once and registered with the local MCP server, so the agent can use it without a restart. Accepted shapes — a DSN, a JDBC URL, an Oracle TNS descriptor, a JDBC thin descriptor, or a bare database file: ``` postgresql://alice:secret@db.example.com:6543/orders?sslmode=require jdbc:postgresql://localhost:5432/appdb mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4 jdbc:mariadb://10.0.0.5:3307/legacy oracle://system:oracle@localhost:1521/XEPDB1 jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # service name jdbc:oracle:thin:@ora:1521:ORCL # SID (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1))) jdbc:oracle:thin:@(DESCRIPTION_LIST=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=rac-1)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=ORCLPDB1)))) jdbc:opengauss://h1:30100,h2:30100,h3:30100/demo?targetServerType=master jdbc:sqlite:C:\data\app.db sqlite:///C:/data/chinook.db C:\data\app.db /var/lib/app.sqlite3 ``` A password found inside the URL is sent once and then only stored; it is never echoed back. URL separators inside a password are fine too: `#`, `?` and `/` survive, and `#` works both literally and as `%23`. `id` is immutable once created — rename the *display name* instead (double-click the name on a row). openGauss and GaussDB are their own kind: their SHA256 authentication is outside what libpq negotiates, so the server talks to them with the pure-Python `py_opengauss` driver (installed with the other Python dependencies) and falls back to psycopg only for clusters that still use md5. Paste the cluster URL and every node is kept, in order — the panel shows `h1 +2:30100`. The structure page leaves out the `BIN$…` tables a cluster keeps in its recycle bin. ## Importing from DBeaver The importer looks for `data-sources.json` under `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\`, plus the `General` and Linux/macOS layouts. When the sibling `credentials-config.json` exists it is decrypted with DBeaver's built-in key (AES-128-CBC, zero IV, 16-byte prefix — see [Tatsh/dbeaver-creds](https://github.com/Tatsh/dbeaver-creds)). Ticked entries are copied into the registry, their passwords into `secrets.json`. A few details make real DBeaver workspaces work: - Connections created through the driver dialog often carry only a JDBC URL (`configurationType: URL`) with no `database` field and no `user`. The importer parses the URL for whatever the form fields omit and takes the user from the decrypted credentials. - openGauss/GaussDB entries added with a driver jar are filed as `generic` in `data-sources.json`; the importer recognises them from the `jdbc:opengauss:` / `jdbc:gaussdb:` scheme instead of reporting `unsupported provider: generic`. - A connection already configured is not offered twice. Sources are identified by kind + host + port + database/service/SID + user (SQLite by file path); a match comes back as `alreadyImported`, the checkbox is disabled and the row says *already added as …*. Calling the import route directly does not bypass this: the entry lands in `skipped` instead. - The list has a filter box (name, host or user) and a bar pinned to the top of the view, so *Import selected* stays in reach however long the workspace is. A workspace that never saved passwords is fine: the connection is imported with an empty password and the row shows **no password**. Fill it in once and it stays. ## Testing a connection **Test** performs a real login: SQLite is opened read-only through `node:sqlite`, the other engines go through the Python drivers, which authenticate and run `select 1` (Oracle: `select 1 from dual`). The row then shows the banner, the user and database, and how long the login took: ``` Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok PostgreSQL 15.19 · demo@demo · select version() ok MySQL 8.0.46 · demo@%@demo · select version() ok ``` Wrong passwords now surface the driver's own message (`ORA-01017`, `DPY-6005`, `password authentication failed`) instead of "port reachable". An unsaved form can be tested too. When the Python server cannot start, the probe degrades to a TCP check and says so. A failing row keeps the message inline — expand it with `⌄` for the raw driver text, or press **Copy** to put the whole report on the clipboard: ``` datasource: mysql id: mysql kind: mysql target: 127.0.0.1:3306/demo at: 2026-11-09 21:41:06 UTC+08:00 error: MySQL connection failed: (2003, "Can't connect to MySQL server on '127.0.0.1' …") plugin: dsh-db-mcp 0.8.0 ``` The panel does not repeat probe failures in the banner above the tabs — that strip is for panel operations such as saving, deleting or installing dependencies. ## Groups and bulk actions Every source carries an optional **group** label, editable under *More settings*. - **Create** — type a name in **New group**. - **Rename** — double-click the group name; **delete** drops the label from its members. - **Collapse** — the triangle next to the group name folds the section; an empty group stays visible as a drop target. - **Move** — drag a source row onto a group, or onto **Ungrouped** to clear it. - **Reorder** — drag a row onto another row: the top half of the target puts it before, the bottom half after. Dropping across groups re-groups and repositions in one go. - **Rename a source** — double-click its name; the id never changes. - **Select** — each group header has a select-all box, so grouped sources join bulk work exactly like loose ones. A row itself is selected by its checkbox (the row body no longer toggles, so double-click stays free for renaming). With a selection made, the bulk bar can test, flip read-only, move into a group, or delete (two-step confirmation). ## Structure browser **Structure** on a source row opens a read-only view: tables and views on the left, columns of the selected table on the right (name, type, nullability, default, primary-key marker). Tables are grouped by schema and each group folds open and closed; the filter box above the tree matches table names and schema names. The controller sends the table's schema along with its name, so two tables with the same name in different schemas stay distinct. It is served by `GET /schema`, which only forwards `list_tables` / `describe_table` to the MCP server — the page cannot run SQL, and nothing is cached. The header row keeps the way back to the list, the connection target and the same identifier chip, so the handle the agent needs is in view while you read the tables. Catalog names can outrun the rail — GaussDB alone has views like `global_plancache_status`. A long name truncates with an ellipsis and keeps its full value in the tooltip; the kind pill beside it keeps its width. When the panel is too narrow for two columns, the tree moves above the column list instead of squeezing the names. Below the tree there is nothing to switch: reads are never limited by schema. Writes are, and they are chosen on the row itself — see *[Writable schemas](safety.md#writable-schemas)*.