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.zh.md CHANGED
@@ -1,252 +1,71 @@
1
1
  # dsh-db-mcp
2
2
 
3
3
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
4
+ [![npm](https://img.shields.io/npm/v/dsh-db-mcp.svg)](https://www.npmjs.com/package/dsh-db-mcp)
4
5
 
5
6
  [English](README.md) | 中文
6
7
 
7
- 一个 DSH 插件:在侧边栏加一个 **数据库** 入口,配一个数据源管理面板,背后是 Python 写的 MCP 服务——让智能体通过工具查询这些数据库,而不是在页面里执行 SQL。
8
-
9
- - 一个面板管四种引擎:SQLite、PostgreSQL、MySQL、Oracle。
10
- - 一条连接串填满整张表单;口令只存在本机,永不回传给页面。
11
- - 可以把 DBeaver 里已有的连接(连同它加密保存的口令)直接导入。
12
- - 默认只读,两道互相独立的写闸门,加一份审计日志。
13
- - 给智能体的是 8 个 MCP 工具,界面里没有 SQL 编辑器。
14
-
15
- 需求与阶段计划见 [`docs/requirements.md`](docs/requirements.md)。
16
-
17
- ## 现在有什么(P7)
18
-
19
- - **Host 半边**(`index.js` → `src/host.js`):一个 Cordis 插件,通过 DSH 的 web server 提供 `/plugins/db-mcp/api`。
20
-
21
- | 路由 | 用途 |
22
- | ---- | ---- |
23
- | `GET /health` | 插件版本、Host 信息、Python 探测、运行时路径、注册表状态、MCP 状态、设置 |
24
- | `GET /datasources` | 注册表,口令字段已剔除 |
25
- | `GET /schema?datasource=…&table=…[&schema=…]` | 只读结构:表清单,或某张表的列 |
26
- | `GET /groups` | 已声明的分组,加上所有正在使用的分组名 |
27
- | `GET /dbeaver/scan` | DBeaver 里已配置的连接 |
28
- | `GET /mcp/status` | Python 服务所处阶段、工具清单、最后的错误、stderr 尾部 |
29
- | `GET /settings` · `POST /settings` | 全局写开关与审计开关 |
30
- | `GET /audit?limit=N` | 工具调用审计日志的末尾若干条 |
31
- | `GET /python/env` | 解释器、venv 路径、已安装的驱动版本 |
32
- | `POST /python/setup` | 建 venv 并 `pip install -r server/requirements.txt` |
33
- | `POST /datasources/save` | 新建或更新数据源(`id` 不可改) |
34
- | `POST /datasources/delete` | 删除数据源及其口令 |
35
- | `POST /datasources/test` | 真实登录探测:SQLite 走 `node:sqlite`,其余走 Python 驱动 |
36
- | `POST /datasources/bulk` | 对一批数据源做 测试 / 只读 / 分组 / 删除 |
37
- | `POST /groups/save` · `POST /groups/delete` | 新建、重命名、删除分组 |
38
- | `POST /dbeaver/import` | 把选中的 DBeaver 连接复制进注册表 |
39
- | `POST /mcp/start` · `POST /mcp/restart` | 启动或重启 Python MCP 服务 |
40
- | `POST /mcp/call` | 开发辅助:经面板调用某个 MCP 工具 |
41
- | `POST /reload` | 开发辅助:不重启 DSH 重新导入 `src/host.js` |
42
-
43
- - **Client 半边**(`client.js`):侧边栏入口(`db-mcp`,order 25)加主面板——数据源列表、极简的新建/编辑表单(一条连接串)、测试/删除/设为默认、只读的「结构」浏览、DBeaver 导入流程、「重启 MCP」按钮、运行时诊断卡(工具数、全局写开关、venv 与依赖版本及一键安装)、以及「审计日志」卡。文案经客户端 `locale` 服务做中英本地化。
44
- - **Python 服务**(`server/`):`main.py`(FastMCP,8 个工具)、`engines.py`(按引擎连接/内省/查询)、`store.py`(读注册表)。插件用 stdio 拉起它,把它的工具以 `mcp__db__*` 发布到 Host 的 `tools` 服务上——**不**经过 `dsh-mcp-client` 的 Loader 行。
45
-
46
- ### 智能体拿到的工具
47
-
48
- | 工具 | 用途 |
49
- | ---- | ---- |
50
- | `mcp__db__list_datasources` | 已登记的数据源(id、类型、目标、是否只读) |
51
- | `mcp__db__list_tables` | 某个数据源的表/视图 |
52
- | `mcp__db__describe_table` | 列、类型、是否可空、主键 |
53
- | `mcp__db__sample_rows` | 某张表的少量样本 |
54
- | `mcp__db__run_query` | 执行一条语句;只读源拒绝写 |
55
- | `mcp__db__explain_query` | 查询计划 |
56
- | `mcp__db__get_ddl` | `CREATE TABLE` 语句(SQLite 取原文,其余按目录重建) |
57
- | `mcp__db__test_connection` | 真实登录、执行 `select 1`、回报服务器/用户/模式 |
58
-
59
- `datasource` 处处可选:不填就用默认数据源。
60
-
61
- ### Python 环境
62
-
63
- 解释器探测顺序:插件配置 `pythonPath` → `<DSH_HOME>/db-mcp/.venv/Scripts/python.exe` → POSIX 上的同一个 venv → `python` → `python3`。依赖装在运行时 venv 里,不随包分发:
64
-
65
- python -m venv C:\Users\<you>\.dsh\db-mcp\.venv
66
- C:\Users\<you>\.dsh\db-mcp\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
67
-
68
- `mcp` 必须固定在 1.x(2.x 把 `FastMCP` 改名成了 `MCPServer`)。
69
-
70
- ## 「测试」按钮是真的登录吗?
71
-
72
- 是。PostgreSQL、MySQL、Oracle 的 **测试** 走 `mcp__db__test_connection`,经 Python MCP 服务执行一次真实认证登录加 `select 1`(Oracle 用 `select 1 from dual`)。列表行随后显示服务器横幅、登录用户与库名、以及登录耗时,例如:
73
-
74
- ```
75
- Oracle AI Database 26ai Free Release 23.26.3.0.0 · DEMO@FREEPDB1 · select 1 from dual ok
76
- PostgreSQL 15.19 · demo@demo · select version() ok
77
- MySQL 8.0.46 · demo@%@demo · select version() ok
78
- ```
79
-
80
- 口令错误或端口不通时,显示的是驱动自己的信息(`ORA-01017`、`DPY-6005`、`password authentication failed`),而不是笼统的「端口可达」。未保存的表单也能测:`test_connection` 接受 `kind` + `config` + `password`,不需要先写注册表。Python 服务起不来时,探测退回 TCP 检查,并明确说明这一点。
81
-
82
- ## 一条连接串填完整张表单
83
-
84
- 新建/编辑表单刻意做得和 DBeaver 的连接对话框一样小:**连接串**、**用户**、**口令**(带「保存密码」勾选),就这些。粘贴 JDBC URL、DSN、Oracle TNS 描述符或 `.db` 路径,类型、主机、端口、库名/服务名、名称与标识符都会自动带出;推导出来的值默认收在「更多设置」里,与模式、SID、SSL 模式、字符集、只读开关放在一起。
85
-
86
- 支持的写法(完整矩阵见 `dev/parse.test.mjs`):
8
+ 一个 [DSH](https://github.com/deepseek-ai/deepseek-harness) 插件,把数据库交给智能体:侧栏多出一个**数据库**面板,登记过的每个数据源都会变成 `mcp__db__*` 工具。
87
9
 
88
- ```
89
- postgresql://alice:secret@db.example.com:6543/orders?sslmode=require
90
- jdbc:postgresql://localhost:5432/appdb
91
- mysql://root:pw@127.0.0.1:3306/shop?charset=utf8mb4
92
- jdbc:mariadb://10.0.0.5:3307/legacy
93
- oracle://system:oracle@localhost:1521/XEPDB1
94
- jdbc:oracle:thin:@//ora:1521/ORCLPDB1 # 服务名
95
- jdbc:oracle:thin:@ora:1521:ORCL # SID
96
- (DESCRIPTION=(ADDRESS=(HOST=tns-host)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=svc1)))
97
- jdbc:sqlite:C:\data\app.db
98
- sqlite:///C:/data/chinook.db
99
- C:\data\app.db / /var/lib/app.sqlite3
100
- ```
101
-
102
- 连接串里的口令只发送一次给 `POST /datasources/save`,之后只存在 `secrets.json` 里,永不回显。
103
-
104
- 写操作一律用 `POST`(同源校验比较 `Origin` 与 `Host`)。口令不会进 `datasources.json`,只存在 `secrets.json`,页面拿到的唯一信号是 `hasSecret`。
105
-
106
- ## 从 DBeaver 导入
107
-
108
- `GET /dbeaver/scan` 会在 `%APPDATA%\DBeaverData\workspace*\General\.dbeaver\` 下找 `data-sources.json`(同时兼容 `General` 布局与 Linux/macOS 位置)。同级存在 `credentials-config.json` 时,用 DBeaver 内置的密钥解密(AES-128-CBC、零 IV、前 16 字节前缀,参考 `Tatsh/dbeaver-creds`)。从未保存过口令的工作区,导入出来的连接口令为空,界面显示「无密码」。
109
-
110
- 通过 DBeaver 驱动对话框建的连接,往往只有一条 JDBC URL(`configurationType: URL`),既没有 `database` 字段也没有 `user` 字段。导入器因此会自己解析 `jdbc:mysql://host:port/db`、`jdbc:postgresql://…`、`jdbc:oracle:thin:@…`,补上表单字段缺的部分,并在解密凭据里存了用户名时从那里取。导入的数据源按 DBeaver 连接名命名(再 slug 成 id),保持 `readOnly: true`,已存的口令会复制进 `secrets.json`;仍然缺的东西写进 `warnings`。
111
-
112
- 已经配置过的连接不会被导入两次:每个数据源用 类型 + 主机 + 端口 + 库名/服务名/SID + 用户 做身份(SQLite 用文件路径),命中的 DBeaver 条目会带上 `alreadyImported: true`、`duplicateId`、`duplicateName` 返回。面板把它的复选框禁用并标注「已作为 … 添加」,`POST /dbeaver/import` 则把这类 id 放进 `skipped`,不会建出第二份。
10
+ **零配置。** 不需要改 DSH 里的任何文件,也不用手工抄一遍连接信息——直接把 DBeaver 里已有的连接导进来,然后开口问就行。
113
11
 
114
- 面板用标签栏导航(*数据源列表 / 新建数据源 / 从 DBeaver 导入 / 运行时诊断 / 审计日志*):当前步骤是纯文本、可回退的步骤是链接,任何页面都不需要额外的「返回列表」按钮。
12
+ ## 三个方面都不用配
115
13
 
116
- ## 用 Docker 起测试库
14
+ - **不用改 DSH 文件。** bundle 的 patch 只插入一行 Loader 记录;插件自己用 stdio 拉起 Python MCP 服务,并把工具注册到 Host 的 `tools` 服务上——不需要 `dsh-mcp-client` 那一行,也不用编辑 JSON、导环境变量。
15
+ - **不用填数据源。** 导入器读取 DBeaver 工作区的 `data-sources.json`,再用 DBeaver 自带的密钥解密 `credentials-config.json`,主机、端口、库名、用户、已存口令一次到位。勾一个框就是全部配置工作。
16
+ - **不用准备 Python。** 面板会自己找解释器、建 venv、装四个驱动包,入口在 **运行时诊断 → 安装 / 修复依赖**。
117
17
 
118
- 宿主机什么都不用装,下面每个引擎都是用完即弃的容器,演示账号 `demo` / `dbmcp-demo`。
18
+ ## 运行
119
19
 
20
+ ```sh
21
+ dsh plugin --profile desktop add dsh-db-mcp # 桌面端
22
+ dsh plugin --profile web add dsh-db-mcp # Web 端
120
23
  ```
121
- # PostgreSQL —— 镜像最小、启动最快,推荐第一个试
122
- docker run -d --name dbmcp-pg -p 5432:5432 \
123
- -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=dbmcp-demo -e POSTGRES_DB=demo \
124
- postgres:15-alpine
125
-
126
- # MySQL
127
- docker run -d --name dbmcp-mysql -p 3306:3306 \
128
- -e MYSQL_USER=demo -e MYSQL_PASSWORD=dbmcp-demo -e MYSQL_DATABASE=demo \
129
- -e MYSQL_ROOT_PASSWORD=dbmcp-root mysql:8.0-debian
130
-
131
- # Oracle Free —— 镜像好几个 GB,要一两分钟才就绪
132
- docker run -d --name dbmcp-oracle -p 1521:1521 -e ORACLE_PASSWORD=dbmcp-demo \
133
- gvenzl/oracle-free:23-slim
134
- ```
135
-
136
- Windows 上 1521 常常落在 Hyper-V 的保留端口段里,`docker run` 会报「尝试访问套接字的方式被其访问权限禁止」。用 `netsh interface ipv4 show excludedportrange protocol=tcp` 查一下,换一个宿主端口发布即可,例如 `-p 11521:1521`,数据源里填 11521。
137
24
 
138
- 然后在面板里用对应的连接串建数据源,例如
139
- `postgresql://demo:dbmcp-demo@127.0.0.1:5432/demo`、
140
- `mysql://demo:dbmcp-demo@127.0.0.1:3306/demo` 或
141
- `oracle://system:dbmcp-demo@127.0.0.1:1521/FREEPDB1`。
25
+ `--profile` 指定装进哪个 profile(随 DSH 提供的是 `desktop` 与 `web`,其它名字首次使用时自动创建)。卸载用
26
+ `dsh plugin --profile desktop remove dsh-db-mcp`。
142
27
 
143
- ## 装进一个 profile
28
+ 然后:
144
29
 
145
- ```
146
- plugin_manager install_bundle target=<本目录的绝对路径>
147
- ```
30
+ 1. 侧栏 → **数据库**。
31
+ 2. **从 DBeaver 导入** → 勾选连接 → **导入所选**。
32
+ 3. 问智能体一句:*「我的 Chinook 库里有哪些表?」*
148
33
 
149
- 或者从 npm 装:
150
-
151
- ```
152
- dsh plugin add dsh-db-mcp@latest
153
- ```
34
+ 如果某条连接在 DBeaver 里没保存口令,卡片会显示**无密码**;打开该数据源补一次即可——口令只写进 `secrets.json`,不会再回到页面上。
154
35
 
155
- bundle patch(`cordis.patch.yml`)只插入一行 Loader 配置,id 为 `db-mcp`。以链接方式装入的 profile 改文件即生效;Host 模块每个进程只导入一次,所以改完 `src/host.js` 要 `POST /plugins/db-mcp/api/reload`(或重启应用)。
36
+ 想从零开始也行:**新建数据源**里粘一条连接串(`postgresql://user:pw@host:5432/db`、JDBC URL、Oracle TNS 描述符或 `.db` 路径),表单会自动填好。
156
37
 
157
- ## 面板布局
158
-
159
- 页面与其它官方面板保持一致的外壳:
160
-
161
- - 页头:**‹ 返回会话**(经 `ctx.layout.selectPanel(null)` 关闭面板)、**数据库** 标题,以及连接状态与版本药丸;
162
- - 标签栏 —— **数据源列表 / 新建数据源 / 从 DBeaver 导入 / 运行时诊断 / 审计日志**,当前标签带下划线。
163
-
164
- 点「新建数据源」或「从 DBeaver 导入」只切换标签下方那张卡片,页头与标签栏不动。编辑复用「新建数据源」标签;结构浏览是「数据源列表」下的子视图,带自己的 `← 数据源` 链接。没有第二排操作栏:**刷新** 与 **重启 MCP** 在数据源列表卡片的头部,分组相关的操作归列表自己管。
165
-
166
- ## 结构概览
167
-
168
- 数据源行上的「结构」按钮打开只读浏览器:左边是该源的表(或视图),右边是选中表的列——名称、类型、可空、默认值、主键标记。数据由 `GET /schema` 提供,它只把 `list_tables` / `describe_table` 转发给 MCP 服务,页面本身永远执行不了 SQL。不缓存:每次打开都读实时目录。
169
-
170
- ## 分组与批量操作
171
-
172
- 每个数据源带一个可选的**分组**标签(注册表里的 `group`,可在「更多设置」里改)。分组归用户所有:在「新建分组」框里输入名字即可创建,**双击组名改名**(或按删除按钮删组),把数据源行拖到某个组上即可移过去(拖到「未分组」上则清除标签)。双击数据源名只改这一条的名字,id 永不改变。列表按组渲染成若干区块,组名排序、未分组的排最后;没有分组时列表保持原来的平铺样式。
173
-
174
- 每个组头都有全选复选框,所以组内的数据源和散在外面的完全一样,都能参与批量操作。勾选任意行即出现批量操作条:批量测试、设为只读或允许写、移动到某个分组、或删除(两步确认)。批量移动走 `POST /datasources/bulk`,body 形如 `{ action: 'test' | 'readOnly' | 'group' | 'delete', ids: [...], … }`;分组的增删改走 `GET /groups`、`POST /groups/save`、`POST /groups/delete`。
175
-
176
- ## 发布
177
-
178
- 包由 GitHub Actions 通过 [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) 发布:`.github/workflows/publish.yml` 用 workflow 的 OIDC token 换短期发布凭据,因此仓库里没有任何 `NPM_TOKEN` 密钥。
179
-
180
- ```bash
181
- node dev/bump.mjs 0.6.5 # 同时改 package.json 与 src/host.js
182
- git commit -am "chore: release 0.6.5"
183
- git tag v0.6.5 && git push origin main --tags
184
- ```
185
-
186
- workflow 会拒绝 tag 与 `package.json` 版本不一致的情况,先跑可移植测试(`npm run test:ci`),再带 `--provenance` 发布,最后轮询 registry 确认版本已出现。首次发布(包还没有 trusted publisher 时)仍可本地 `npm publish`。
187
-
188
- ## 开发
189
-
190
- ```
191
- node dev/engines.test.mjs # 11 项:engines.py 的 commit/rollback 与 affectedRows,用假连接
192
- node dev/selftest.mjs # 在临时端口挂载 src/host.js,46 项断言
193
- node dev/shims.test.mjs # 挂载 index.js,检查路由、/reload、工具发布
194
- node dev/parse.test.mjs # 24 项:连接串解析(DSN/JDBC/TNS/路径)
195
- node dev/render.test.mjs # 40 项:用 stub React 渲染组件与结果渲染器
196
- node dev/i18n.test.mjs # 12 项:中英键集合一致、无死键、无漏译
197
- node dev/p5.test.mjs # 15 项:设置、审计日志、Python 环境
198
- node dev/mcp.test.mjs # 19 项:真实 Python 服务走 stdio,真实 SQLite 与 Oracle 登录
199
- ```
200
-
201
- `npm test` 跑除 `mcp.test.mjs` 之外的全部(后者需要 venv 与 Oracle 容器)。`dev/engines.test.mjs` 只需要一个 Python 解释器(插件 venv,否则 PATH 里的 `python`),不连数据库:它用假 DB-API 连接驱动 `server/engines.py`,守住 `run_query` 的 commit/rollback 与 `affectedRows` 契约。`node dev/bump.mjs 0.6.6` 会同步两处版本号。
202
-
203
- ## 安全闸门
204
-
205
- 是否允许写入由两道互相独立的闸门决定:
206
-
207
- 1. **全局开关** —— `<运行时目录>/settings.json` 的 `allowWrite`,在诊断卡上切换。默认关闭;关闭期间所有写语句都以 `user_error` 拒绝,并给出指向该开关的提示。
208
- 2. **单源标记** —— 数据源的 `readOnly`,默认开启。即使全局开关打开,只要不清掉这条标记,该源仍然只读。点数据源行上的「只读 / 可写」药丸即可一键切换:该源可写时药丸变成琥珀色,若全局开关仍关着,提示条会提醒你去打开它。
209
-
210
- Python 服务启动时从环境变量 `DB_MCP_ALLOW_WRITE` 读取全局标记;开关一变,Host 就会重启它。只要引擎支持,只读还会在驱动层再兜一次(SQLite 的 `mode=ro`、PostgreSQL 的 `default_transaction_read_only`、MySQL 的 `SET SESSION TRANSACTION READ ONLY`)。
211
-
212
- ### 插件会启动什么,以及带着什么
213
-
214
- - Python 服务用固定的解释器路径、固定脚本、固定参数启动,**从不经过 shell**,所以任何字符串都不会被 `cmd.exe` 或 `/bin/sh` 再解析一次。
215
- - 子进程**不继承** Host 的环境变量。它只拿到一张很短的允许清单(`PATH`、`TEMP`、`HOME`、`PYTHON*` 之类)再加上插件自己设置的 `DB_MCP_*`。DSH 进程里的 token、云凭据不会被带下去。
216
- - PostgreSQL / MySQL / Oracle 都由这个子进程连接,只有 SQLite 是在进程内读的。裸 TCP 探测只是子进程起不来时的兜底,且不发任何凭据。
217
- - 口令存在 `secrets.json`(`0600`),任何 HTTP 接口都不会返回它;面板只知道 `hasSecret`。
38
+ ## 智能体拿到的工具
218
39
 
40
+ | 工具 | 作用 |
41
+ | ---- | ---- |
42
+ | `mcp__db__list_datasources` | 已登记的数据源:id、类型、目标、是否只读 |
43
+ | `mcp__db__list_tables` | 某个源里的表与视图 |
44
+ | `mcp__db__describe_table` | 列、类型、可空、主键 |
45
+ | `mcp__db__sample_rows` | 取几行样本 |
46
+ | `mcp__db__execute_sql` | 执行一条语句——查询,或在允许写入时执行写入 |
47
+ | `mcp__db__explain_query` | 执行计划 |
48
+ | `mcp__db__get_ddl` | `CREATE TABLE` 语句 |
49
+ | `mcp__db__test_connection` | 真实登录 + `select 1`,回报服务器/用户/schema |
219
50
 
220
- ## 审计日志
51
+ `datasource` 在所有工具里都是可选的,省略就用默认数据源。查询发生在**对话里**,面板只负责管理连接,不是 SQL 控制台。
221
52
 
222
- 每次 MCP 工具调用都会向 `<运行时目录>/logs/audit.jsonl`(0600)追加一行 JSON:时间、工具、数据源、耗时、行数、截断到 2000 字符的 SQL,以及失败时的错误。结果行永不记录。文件超过 512 KiB 轮转为 `audit.1.jsonl`,也可以在 `settings.json` 里用 `auditLog: false` 关掉。面板显示最近十几条,时间按**本机时区**换算——文件里存的是 UTC,这样换机器也仍然可比;卡片会把每条时间戳转成浏览者的时区,并标出所用偏移。
53
+ ## 安全
223
54
 
224
- `dev/shims.test.mjs` 还断言 `package.json` 不带 UTF-8 BOM:Host 用 `JSON.parse` 读它,而 `JSON.parse` 会拒绝 BOM,插件随之显示为 `availability: missing`,并不加载。会「添加 BOM」的编辑器会静默弄坏 bundle——如果重启后侧边栏入口消失,先查这一项。
55
+ - **默认只读**,两道互相独立的闸门:全局写开关 + 单源标记,两者都打开才允许写入。
56
+ - **口令不出本机。** 存在 `secrets.json`(`0600`);任何 HTTP 接口都不返回它,Python 子进程拿到的也只是很短的环境变量白名单,而不是整个 DSH 环境。
57
+ - **每次工具调用都有审计**,写入 `logs/audit.jsonl`——工具、数据源、耗时、行数、SQL;结果行永不记录。
58
+ - **面板不会执行 SQL。** 结构浏览只转发 `list_tables` 与 `describe_table`。
225
59
 
226
- ## 阶段表
60
+ ## 文档
227
61
 
228
- | 阶段 | 内容 |
62
+ | 文档 | 内容 |
229
63
  | ---- | ---- |
230
- | P1 | 可安装的骨架:侧边栏入口、空面板、Host 健康接口(已完成) |
231
- | P2 | 数据源 CRUD、连接测试、DBeaver 导入(已完成) |
232
- | P3 | Python MCP 服务,工具以 `mcp__db__*` 发布(已完成) |
233
- | P4 | PostgreSQL / MySQL / Oracle 方言(已完成) |
234
- | P5 | 诊断、一键 venv、热重载、审计日志、全局写开关(已完成) |
235
- | P6 | 多语言打磨与文档(已完成) |
236
- | P7 | 只读结构概览、分组、批量操作(已完成) |
64
+ | [docs/guide.md](docs/guide.md) | 面板操作、连接串、分组、Docker 测试库、排错(英文) |
65
+ | [docs/reference.md](docs/reference.md) | HTTP 接口、工具契约、注册表与设置文件、插件配置(英文) |
66
+ | [docs/development.md](docs/development.md) | 目录结构、热重载、测试套件、发布流程(英文) |
67
+ | [docs/design-notes.md](docs/design-notes.md) | 设计日志(P1 → P7)与决策记录(英文) |
237
68
 
238
69
  ## 许可
239
70
 
240
71
  Apache License 2.0 —— 见 [LICENSE](LICENSE)。
241
-
242
- ## 运行时目录
243
-
244
- ```
245
- $DSH_HOME/db-mcp/
246
- datasources.json # 数据源档案,不含口令,0600
247
- secrets.json # 各源口令,0600,仅 Host 可读
248
- settings.json # 全局写开关 + 审计开关,0600
249
- logs/mcp.log # 已掩码的日志
250
- logs/audit.jsonl # 每次 MCP 工具调用一行,超过 512 KiB 轮转
251
- .venv/ # 插件内 venv:mcp、psycopg、PyMySQL、oracledb
252
- ```
@@ -1,3 +1,8 @@
1
+ > **Design log.** The working record of the design and the P1 → P7 build, written in
2
+ > Chinese as the project ran. It is kept for the decisions and the edge cases behind
3
+ > them, not as user documentation: start at [../README.md](../README.md), or
4
+ > [guide.md](guide.md) for usage and [reference.md](reference.md) for the wire format.
5
+
1
6
  # DSH 数据库插件(暂名 `dsh-db-mcp`)需求细化 v0.2
2
7
 
3
8
  > 状态:需求已按你的选择定稿,代码尚未开始
@@ -0,0 +1,129 @@
1
+ # Development
2
+
3
+ How the repository is put together, how to iterate on it without restarting DSH
4
+ every time, and how a release happens.
5
+
6
+ ## Layout
7
+
8
+ ```
9
+ index.js Cordis entry: routes only, plus the cache-busting import of src/host.js
10
+ src/host.js Host half: registry, DBeaver import, probes, settings, audit, MCP bridge wiring
11
+ src/mcp.js minimal MCP client over stdio (newline-delimited JSON-RPC)
12
+ src/tools.js spawns the Python server, publishes its tools on the host tools service
13
+ client.js browser half: sidebar entry + panel (React, theme tokens only)
14
+ locale/{en,zh}.json package metadata (title/description) shown by the plugin card
15
+ server/main.py FastMCP server: eight tools
16
+ server/engines.py per-engine connect / introspect / query / DDL
17
+ server/store.py reads the registry written by the Host
18
+ cordis.patch.yml one Loader row with id "db-mcp"
19
+ dev/ test suites and the version bump script
20
+ docs/ this documentation
21
+ ```
22
+
23
+ The package ships `index.js`, `src/*.js`, `client.js`, `locale/*.json`, `icon.svg`,
24
+ `cordis.patch.yml`, `server/*.py`, `server/requirements.txt`, `docs/*.md`, both
25
+ READMEs and the license.
26
+
27
+ ## Hot reload
28
+
29
+ The Cordis Loader imports a plugin entry **once per process**, so editing
30
+ `index.js` needs a DSH restart. Everything else is deliberately reloadable:
31
+
32
+ - `index.js` only registers two routes and imports `./src/host.js?v=<n>`, so
33
+ `POST /plugins/db-mcp/api/reload` re-imports the whole Host half.
34
+ - `src/host.js` and `src/tools.js` propagate the same generation string to their own
35
+ imports (`new URL(import.meta.url).searchParams.get('v')`), otherwise Node's module
36
+ cache would keep returning the old copies of the inner files.
37
+ - The Python server is a child process: `POST /mcp/restart` (or the **Restart MCP**
38
+ button) replaces it. Tool registrations stay in place — disposing and re-registering
39
+ them makes the agent's tool catalog go stale and report `unknown tool`.
40
+
41
+ A profile that links this directory (`dsh plugin --profile desktop add <path>`)
42
+ reflects edits immediately; one installed from npm needs a release.
43
+
44
+ ## Tests
45
+
46
+ ```sh
47
+ npm test # everything below except mcp.test.mjs
48
+ npm run test:ci # the portable subset the publish workflow runs
49
+ npm run test:mcp # real Python server, real SQLite; Oracle when its container is up
50
+ npm run test:engines # server/engines.py against a stub DB-API connection
51
+ ```
52
+
53
+ | Suite | Covers |
54
+ | ----- | ------ |
55
+ | `dev/engines.test.mjs` + `engines.test.py` | commit/rollback and `affectedRows` contract of `execute_sql`, no database needed |
56
+ | `dev/selftest.mjs` | mounts `src/host.js` on a throwaway port: registry, validation, groups, bulk, schema route, DBeaver scan, probes |
57
+ | `dev/shims.test.mjs` | mounts `index.js`, checks both routes, `/reload`, tool publication, and that `package.json` has no BOM |
58
+ | `dev/parse.test.mjs` | the connection-string parser (DSN, JDBC, TNS, SID, paths) |
59
+ | `dev/render.test.mjs` | components rendered with a stub React: tab bar, form fields, list, bulk bar, breadcrumbs |
60
+ | `dev/i18n.test.mjs` | en/zh key parity, no dead keys, no stray copy outside the dictionaries |
61
+ | `dev/p5.test.mjs` | settings, audit rotation, Python environment discovery |
62
+ | `dev/mcp.test.mjs` | real stdio handshake, all eight tools, read-only refusal, login failures |
63
+
64
+ Suites that need external services skip rather than fail: `mcp.test.mjs` probes the
65
+ Oracle port and prints `skip … is not listening` when the container is down.
66
+
67
+ ## Releasing
68
+
69
+ The package publishes from GitHub Actions with
70
+ [npm trusted publishing](https://docs.npmjs.com/trusted-publishers):
71
+ `.github/workflows/publish.yml` trades the workflow's OIDC token for a short-lived
72
+ publish credential, so no `NPM_TOKEN` secret exists.
73
+
74
+ ```sh
75
+ node dev/bump.mjs 0.7.0 # package.json + src/host.js, both
76
+ git commit -am "chore: release 0.7.0"
77
+ git push origin main
78
+ git tag v0.7.0 && git push origin v0.7.0
79
+ ```
80
+
81
+ The workflow refuses to publish when the tag and `package.json` disagree, runs
82
+ `npm run test:ci`, publishes with `--provenance`, then polls the registry until the
83
+ version appears. Local `npm publish` still works for the very first release, before
84
+ a trusted publisher exists.
85
+
86
+ Keep `src/host.js`'s `PLUGIN_VERSION` in step with `package.json` — the panel and
87
+ `/health` read it, and a mismatch ships a package that reports the previous version.
88
+
89
+ Installing that release into a profile (the profile name is required, the verb is
90
+ pnpm's):
91
+
92
+ ```sh
93
+ dsh plugin --profile desktop add dsh-db-mcp@0.7.0
94
+ dsh plugin --profile desktop remove dsh-db-mcp
95
+ ```
96
+
97
+ Expect pnpm to print `Added 1 entry to minimumReleaseAgeExclude …` when the version
98
+ is younger than its 1440-minute cooldown: that line is pnpm recording a per-version
99
+ exception so the explicit pin may install. See
100
+ [guide.md](guide.md#install-and-upgrade) for the policy knobs.
101
+
102
+ ## When git says "not a git repository"
103
+
104
+ Three times on this machine `git status` answered `fatal: not a git repository` while
105
+ `.git/objects` still held every commit: `HEAD`, `config` and `index` had vanished,
106
+ and git refuses to run at all without `HEAD` (even with an explicit `--git-dir`).
107
+ The history was never lost, so it can be rebuilt from the refs:
108
+
109
+ ```sh
110
+ node dev/git-repair.mjs --check # report what is missing
111
+ node dev/git-repair.mjs # write HEAD + config, then git reset to rebuild the index
112
+ ```
113
+
114
+ The remote URL comes from `package.json`'s `repository`, and the branch is the one
115
+ with the newest ref, so the script stays machine independent. It is a repair, not a
116
+ backup: if `.git/objects` itself is missing, restore from a bundle instead
117
+ (`git bundle create backup.bundle --all` in a healthy checkout, then `git clone`).
118
+
119
+ ## Adding an engine
120
+
121
+ 1. `server/engines.py`: extend `connect()` and the dialect tables (`_quote`,
122
+ `_limit_clause`, `list_tables`, `describe_table`, `table_ddl`).
123
+ 2. `src/host.js`: add the kind to `KINDS`, give it a default port, a validation
124
+ branch in `normalizeInput`, and a `describeTarget` label.
125
+ 3. `client.js`: add the string to `KINDS` and, if the form needs extra fields, to
126
+ `SourceForm`; extend `parseConnectionText` with the scheme and JDBC prefix.
127
+ 4. `server/requirements.txt`: the driver, and `docs/reference.md`: the field table.
128
+ 5. Tests: `dev/parse.test.mjs` for the string, `dev/selftest.mjs` for validation and
129
+ the probe, and a demo container in `docs/guide.md`.