dsh-db-mcp 0.6.8 → 0.6.9
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 +15 -0
- package/README.zh.md +8 -0
- package/package.json +1 -1
- package/src/host.js +1 -1
- package/src/mcp.js +45 -1
package/README.md
CHANGED
|
@@ -310,6 +310,21 @@ driver itself where the engine allows it (`mode=ro` for SQLite,
|
|
|
310
310
|
`default_transaction_read_only` for PostgreSQL, `SET SESSION TRANSACTION READ ONLY`
|
|
311
311
|
for MySQL).
|
|
312
312
|
|
|
313
|
+
### What the plugin runs, and with what
|
|
314
|
+
|
|
315
|
+
- The Python server is started with a fixed interpreter path, a fixed script and a
|
|
316
|
+
fixed argument list — never through a shell, so no string is ever re-parsed by
|
|
317
|
+
`cmd.exe` or `/bin/sh`.
|
|
318
|
+
- The child does **not** inherit the host environment. It gets a short allowlist
|
|
319
|
+
(`PATH`, `TEMP`, `HOME`, `PYTHON*`, and similar) plus the three `DB_MCP_*` flags
|
|
320
|
+
the plugin sets. Tokens or cloud credentials in the DSH process are not passed
|
|
321
|
+
down.
|
|
322
|
+
- Connections to PostgreSQL, MySQL and Oracle are opened by that child; only SQLite
|
|
323
|
+
is read in-process. The raw TCP probe is a fallback used when the child cannot
|
|
324
|
+
start, and it never sends credentials.
|
|
325
|
+
- Passwords live in `secrets.json` (mode `0600`) and are never returned by any HTTP
|
|
326
|
+
route; the panel only learns `hasSecret`.
|
|
327
|
+
|
|
313
328
|
## Audit log
|
|
314
329
|
|
|
315
330
|
Every MCP tool call appends one JSON line to `<runtime>/logs/audit.jsonl` (0600):
|
package/README.zh.md
CHANGED
|
@@ -209,6 +209,14 @@ node dev/mcp.test.mjs # 19 项:真实 Python 服务走 stdio,真实 SQLi
|
|
|
209
209
|
|
|
210
210
|
Python 服务启动时从环境变量 `DB_MCP_ALLOW_WRITE` 读取全局标记;开关一变,Host 就会重启它。只要引擎支持,只读还会在驱动层再兜一次(SQLite 的 `mode=ro`、PostgreSQL 的 `default_transaction_read_only`、MySQL 的 `SET SESSION TRANSACTION READ ONLY`)。
|
|
211
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`。
|
|
218
|
+
|
|
219
|
+
|
|
212
220
|
## 审计日志
|
|
213
221
|
|
|
214
222
|
每次 MCP 工具调用都会向 `<运行时目录>/logs/audit.jsonl`(0600)追加一行 JSON:时间、工具、数据源、耗时、行数、截断到 2000 字符的 SQL,以及失败时的错误。结果行永不记录。文件超过 512 KiB 轮转为 `audit.1.jsonl`,也可以在 `settings.json` 里用 `auditLog: false` 关掉。面板显示最近十几条,时间按**本机时区**换算——文件里存的是 UTC,这样换机器也仍然可比;卡片会把每条时间戳转成浏览者的时区,并标出所用偏移。
|
package/package.json
CHANGED
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.
|
|
36
|
+
const PLUGIN_VERSION = '0.6.9'
|
|
37
37
|
const SCHEMA_VERSION = 1
|
|
38
38
|
const REGISTRY_FILE = 'datasources.json'
|
|
39
39
|
const SECRETS_FILE = 'secrets.json'
|
package/src/mcp.js
CHANGED
|
@@ -16,6 +16,50 @@ import { spawn } from 'node:child_process';
|
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
17
|
const INIT_TIMEOUT_MS = 60_000;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The Python child gets a small, explicit environment instead of a copy of ours.
|
|
21
|
+
* Spreading `process.env` would hand every token and cloud credential in the
|
|
22
|
+
* DSH process to a driver stack that only ever needs to find its interpreter.
|
|
23
|
+
*/
|
|
24
|
+
const ENV_ALLOW = [
|
|
25
|
+
'PATH',
|
|
26
|
+
'Path',
|
|
27
|
+
'PATHEXT',
|
|
28
|
+
'SystemRoot',
|
|
29
|
+
'SYSTEMROOT',
|
|
30
|
+
'windir',
|
|
31
|
+
'ComSpec',
|
|
32
|
+
'COMSPEC',
|
|
33
|
+
'TEMP',
|
|
34
|
+
'TMP',
|
|
35
|
+
'TMPDIR',
|
|
36
|
+
'HOME',
|
|
37
|
+
'USERPROFILE',
|
|
38
|
+
'HOMEDRIVE',
|
|
39
|
+
'HOMEPATH',
|
|
40
|
+
'APPDATA',
|
|
41
|
+
'LOCALAPPDATA',
|
|
42
|
+
'PROGRAMDATA',
|
|
43
|
+
'PROGRAMFILES',
|
|
44
|
+
'LANG',
|
|
45
|
+
'LC_ALL',
|
|
46
|
+
'TZ',
|
|
47
|
+
'PWD',
|
|
48
|
+
'PYTHONPATH',
|
|
49
|
+
'PYTHONHOME',
|
|
50
|
+
'PYTHONIOENCODING',
|
|
51
|
+
'VIRTUAL_ENV',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function childEnvironment(extra) {
|
|
55
|
+
const env = {};
|
|
56
|
+
for (const key of ENV_ALLOW) {
|
|
57
|
+
const value = process.env[key];
|
|
58
|
+
if (typeof value === 'string' && value !== '') env[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return Object.assign(env, extra || {});
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
export class McpError extends Error {
|
|
20
64
|
constructor(message, { code = 'mcp_error', data } = {}) {
|
|
21
65
|
super(message);
|
|
@@ -61,7 +105,7 @@ export class McpClient {
|
|
|
61
105
|
if (this.#child) return this;
|
|
62
106
|
const child = spawn(this.command, this.args, {
|
|
63
107
|
cwd: this.cwd,
|
|
64
|
-
env:
|
|
108
|
+
env: childEnvironment(this.env),
|
|
65
109
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
110
|
windowsHide: true,
|
|
67
111
|
});
|