dsh-dbhub-live 3.0.0 → 3.1.3
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/AGENTS.md +22 -10
- package/README.en.md +168 -0
- package/README.md +63 -29
- package/doc/REQUIREMENTS.md +33 -5
- package/lib/adhoc.mjs +96 -80
- package/lib/client.js +498 -71
- package/lib/config.mjs +151 -16
- package/lib/i18n.mjs +139 -0
- package/lib/index.mjs +165 -37
- package/lib/mcp.mjs +120 -129
- package/lib/options.mjs +94 -0
- package/lib/runtime.mjs +14 -16
- package/lib/tools.mjs +463 -303
- package/package.json +6 -5
- package/test/adhoc.test.mjs +29 -0
- package/test/client-format.test.mjs +49 -1
- package/test/i18n.test.mjs +27 -0
- package/test/init.test.mjs +162 -105
- package/test/options.test.mjs +41 -0
- package/test/util.test.mjs +111 -95
package/lib/adhoc.mjs
CHANGED
|
@@ -1,81 +1,97 @@
|
|
|
1
|
-
// dsh-dbhub-live: ad-hoc temporary connections.
|
|
2
|
-
//
|
|
3
|
-
// Each call spawns a fresh throwaway `dbhub --transport stdio --dsn <dsn>`,
|
|
4
|
-
// runs one MCP tools/call against an ad-hoc target (ip/account/password/db
|
|
5
|
-
// supplied by the model per request), then kills it. Independent per call, so
|
|
6
|
-
// two parallel calls can query two different databases at once. Nothing is
|
|
7
|
-
// persisted and the persistent multi-source server is untouched.
|
|
8
|
-
|
|
9
|
-
import { DATA_DIR, maskDsn } from './config.mjs'
|
|
10
|
-
import { buildSpawnArgv, resolveDbhubExe } from './runtime.mjs'
|
|
11
|
-
import { createMcpClient, disabledMessage } from './mcp.mjs'
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
1
|
+
// dsh-dbhub-live: ad-hoc temporary connections.
|
|
2
|
+
//
|
|
3
|
+
// Each call spawns a fresh throwaway `dbhub --transport stdio --dsn <dsn>`,
|
|
4
|
+
// runs one MCP tools/call against an ad-hoc target (ip/account/password/db
|
|
5
|
+
// supplied by the model per request), then kills it. Independent per call, so
|
|
6
|
+
// two parallel calls can query two different databases at once. Nothing is
|
|
7
|
+
// persisted and the persistent multi-source server is untouched.
|
|
8
|
+
|
|
9
|
+
import { DATA_DIR, maskDsn } from './config.mjs'
|
|
10
|
+
import { buildSpawnArgv, resolveDbhubExe } from './runtime.mjs'
|
|
11
|
+
import { createMcpClient, disabledMessage } from './mcp.mjs'
|
|
12
|
+
import { currentT } from './i18n.mjs'
|
|
13
|
+
import * as state from './state.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect a surfaced DSN whose password was masked (as in the settings-card
|
|
17
|
+
* summaries). The model sometimes copies a masked DSN into `dbhub_query`;
|
|
18
|
+
* connecting with `****` always fails, so refuse loudly with the right path
|
|
19
|
+
* instead of burning an auth round-trip.
|
|
20
|
+
* @param dsn - the ad-hoc DSN string.
|
|
21
|
+
* @returns whether the password position looks masked (`:**…` before @).
|
|
22
|
+
*/
|
|
23
|
+
export function isMaskedDsn(dsn) {
|
|
24
|
+
return typeof dsn === 'string' && /\/\/[^/@]*:\*{2,}@/.test(dsn)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runAdhoc(subprocess, dsn, rawName, mcpArgs, exec) {
|
|
28
|
+
if (!state.isEnabled()) {
|
|
29
|
+
return { ok: false, text: disabledMessage() }
|
|
30
|
+
}
|
|
31
|
+
if (!dsn || typeof dsn !== 'string' || !dsn.trim()) {
|
|
32
|
+
return { ok: false, text: currentT('result.dsnMissing') }
|
|
33
|
+
}
|
|
34
|
+
if (isMaskedDsn(dsn)) {
|
|
35
|
+
return { ok: false, text: currentT('result.masked') }
|
|
36
|
+
}
|
|
37
|
+
let exe
|
|
38
|
+
try {
|
|
39
|
+
exe = await resolveDbhubExe(subprocess, exec.signal)
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { ok: false, text: '无法获取 dbhub: ' + String((e && e.message) || e) }
|
|
42
|
+
}
|
|
43
|
+
let handle
|
|
44
|
+
try {
|
|
45
|
+
handle = subprocess.spawn({
|
|
46
|
+
argv: buildSpawnArgv(subprocess, exe, ['--transport', 'stdio', '--dsn', dsn.trim()]),
|
|
47
|
+
cwd: DATA_DIR,
|
|
48
|
+
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
|
|
49
|
+
graceMs: 3000,
|
|
50
|
+
signal: exec.signal,
|
|
51
|
+
})
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { ok: false, text: '启动临时 dbhub 失败: ' + String((e && e.message) || e) }
|
|
54
|
+
}
|
|
55
|
+
let stderrTail = ''
|
|
56
|
+
try {
|
|
57
|
+
const client = createMcpClient(handle, (c) => {
|
|
58
|
+
stderrTail = (stderrTail + String(c)).slice(-2000)
|
|
59
|
+
})
|
|
60
|
+
await client.request('initialize', {
|
|
61
|
+
protocolVersion: '2025-03-26',
|
|
62
|
+
capabilities: {},
|
|
63
|
+
clientInfo: { name: 'dsh-dbhub-adhoc', version: '3.0.0' },
|
|
64
|
+
})
|
|
65
|
+
client.notify('notifications/initialized')
|
|
66
|
+
const res = await client.request('tools/call', { name: rawName, arguments: mcpArgs })
|
|
67
|
+
const textOf = (r) => {
|
|
68
|
+
if (r && Array.isArray(r.content)) {
|
|
69
|
+
const parts = []
|
|
70
|
+
for (const b of r.content) if (b && typeof b.text === 'string') parts.push(b.text)
|
|
71
|
+
return parts.join('\n')
|
|
72
|
+
}
|
|
73
|
+
return JSON.stringify(r)
|
|
74
|
+
}
|
|
75
|
+
const prefix = currentT('label.adhocConn', { dsn: maskDsn(dsn) }) + '\n'
|
|
76
|
+
if (res && res.isError) return { ok: false, text: textOf(res) }
|
|
77
|
+
if (res && res.structuredContent !== undefined) {
|
|
78
|
+
return { ok: true, text: prefix + JSON.stringify(res.structuredContent, null, 2) }
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, text: prefix + textOf(res) }
|
|
81
|
+
} catch (e) {
|
|
82
|
+
let detail = String((e && e.message) || e)
|
|
83
|
+
if (stderrTail) detail += '\n[dbhub stderr] ' + stderrTail
|
|
84
|
+
return { ok: false, text: detail }
|
|
85
|
+
} finally {
|
|
86
|
+
try {
|
|
87
|
+
handle.terminate()
|
|
88
|
+
} catch (e) {
|
|
89
|
+
/* ignore */
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await handle.waitForExit(exec.signal)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
81
97
|
}
|