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/README.zh.md
CHANGED
|
@@ -1,252 +1,71 @@
|
|
|
1
1
|
# dsh-db-mcp
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-db-mcp)
|
|
4
5
|
|
|
5
6
|
[English](README.md) | 中文
|
|
6
7
|
|
|
7
|
-
一个 DSH
|
|
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
|
-
|
|
12
|
+
## 三个方面都不用配
|
|
115
13
|
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
28
|
+
然后:
|
|
144
29
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
30
|
+
1. 侧栏 → **数据库**。
|
|
31
|
+
2. **从 DBeaver 导入** → 勾选连接 → **导入所选**。
|
|
32
|
+
3. 问智能体一句:*「我的 Chinook 库里有哪些表?」*
|
|
148
33
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
dsh plugin add dsh-db-mcp@latest
|
|
153
|
-
```
|
|
34
|
+
如果某条连接在 DBeaver 里没保存口令,卡片会显示**无密码**;打开该数据源补一次即可——口令只写进 `secrets.json`,不会再回到页面上。
|
|
154
35
|
|
|
155
|
-
|
|
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
|
-
|
|
53
|
+
## 安全
|
|
223
54
|
|
|
224
|
-
|
|
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
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
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`.
|