dsh-mcp 1.0.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/lib/probe.js ADDED
@@ -0,0 +1,53 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { createTransport } from "./transport.js";
4
+ const DEFAULT_PROBE_TIMEOUT_MS = 15e3;
5
+ function probeErrorMessage(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ async function runProbe(client, config) {
9
+ await client.connect(createTransport(config));
10
+ const tools = [];
11
+ let cursor;
12
+ do {
13
+ const response = await client.request(
14
+ { method: "tools/list", ...cursor === void 0 ? {} : { params: { cursor } } },
15
+ ListToolsResultSchema
16
+ );
17
+ for (const tool of response.tools) {
18
+ tools.push({
19
+ name: tool.name,
20
+ ...tool.description === void 0 ? {} : { description: tool.description }
21
+ });
22
+ }
23
+ cursor = response.nextCursor;
24
+ } while (cursor);
25
+ return { ok: true, tools };
26
+ }
27
+ async function probeConnection(config, options = {}) {
28
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
29
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
30
+ throw new Error(`probeConnection: timeoutMs must be a positive finite number, received ${String(timeoutMs)}`);
31
+ }
32
+ const client = new Client(
33
+ { name: "dsh-mcp-client", version: "0.0.1" },
34
+ { capabilities: {} }
35
+ );
36
+ const timeout = new Promise((_, reject) => {
37
+ const timer = setTimeout(() => reject(new Error(`connection probe timed out after ${timeoutMs}ms`)), timeoutMs);
38
+ timer.unref();
39
+ });
40
+ try {
41
+ return await Promise.race([runProbe(client, config), timeout]);
42
+ } catch (error) {
43
+ return { ok: false, message: probeErrorMessage(error) };
44
+ } finally {
45
+ try {
46
+ await client.close();
47
+ } catch {
48
+ }
49
+ }
50
+ }
51
+ export {
52
+ probeConnection
53
+ };
@@ -0,0 +1,25 @@
1
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
4
+ function buildChildEnv(extra) {
5
+ return { ...scrubbedParentEnv(), ...extra };
6
+ }
7
+ function createTransport(config) {
8
+ switch (config.transport) {
9
+ case "stdio":
10
+ return new StdioClientTransport({
11
+ command: config.command,
12
+ args: config.args,
13
+ env: buildChildEnv(config.env),
14
+ cwd: config.cwd
15
+ });
16
+ case "streamable-http":
17
+ return new StreamableHTTPClientTransport(
18
+ new URL(config.url),
19
+ { requestInit: { headers: config.headers } }
20
+ );
21
+ }
22
+ }
23
+ export {
24
+ createTransport
25
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "dsh-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server 管理插件(独立版):托管 MCP 服务器注册表(持久化定义、运行时挂载、环境变量注入、连接探测)+ Web 设置管理页。由 deepseek-harness 仓库内的 mcp-manager / ui-settings-mcp / web-mcp 迁移合并而来。MCP server registry with WebUI management for DeepSeek Harness.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "scripts",
16
+ "static",
17
+ "README.md",
18
+ "README.en.md",
19
+ "CHANGELOG.md",
20
+ "CHANGELOG.en.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "node scripts/build.mjs"
25
+ },
26
+ "author": "Arvin.qi <arvin.qi@qq.com>",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/ArvinQi/dsh-mcp.git"
31
+ },
32
+ "homepage": "https://github.com/ArvinQi/dsh-mcp",
33
+ "bugs": {
34
+ "url": "https://github.com/ArvinQi/dsh-mcp/issues"
35
+ },
36
+ "dsh": {
37
+ "client": {
38
+ "inject": [
39
+ "@deepseek-ai/dsh-client-runtime",
40
+ "@deepseek-ai/dsh-api-remotes",
41
+ "@deepseek-ai/dsh-client-ui-settings",
42
+ "@deepseek-ai/dsh-client-locale"
43
+ ],
44
+ "platform": "web"
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Build the dsh-mcp client bundle.
3
+ *
4
+ * Produces lib/client.js in the exact wire format the DSH web shell expects:
5
+ * a CJS factory handed to window.__ModuleLoader__.load({ id, factory }), with
6
+ * platform modules resolved through the injected require (the loader module
7
+ * table) and everything else inlined.
8
+ *
9
+ * CSS Modules are handled by an esbuild onLoad plugin: each `.module.css` is
10
+ * rewritten to a JS module that (1) injects the stylesheet text into a
11
+ * <style data-plugin="dsh-mcp"> tag and (2) default-exports an identity class
12
+ * map (class name -> class name; the standalone plugin does not need hashed
13
+ * names because its UI is scoped to the settings section it owns).
14
+ *
15
+ * esbuild is resolved from the DSH source checkout (the only place it is
16
+ * installed); the plugin package itself has zero runtime dependencies.
17
+ * Set DSH_SOURCE to the DSH checkout root when it is not one of the known
18
+ * defaults below.
19
+ */
20
+ import { createRequire } from 'node:module'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { homedir } from 'node:os'
23
+ import { dirname, join } from 'node:path'
24
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
25
+
26
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
27
+
28
+ /** DSH source checkout root; override with $DSH_SOURCE when not a default. */
29
+ function resolveCheckout() {
30
+ if (process.env.DSH_SOURCE && existsSync(process.env.DSH_SOURCE)) return process.env.DSH_SOURCE
31
+ const defaults = [
32
+ join(homedir(), '.dsh/source/current'),
33
+ ]
34
+ for (const candidate of defaults) {
35
+ if (existsSync(candidate)) return candidate
36
+ }
37
+ throw new Error('esbuild not found: set DSH_SOURCE to the DSH checkout root')
38
+ }
39
+
40
+ const CHECKOUT = resolveCheckout()
41
+
42
+ /** Loader entry name — must equal the patch row `name` EXACTLY. */
43
+ const MANIFEST = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
44
+ const PLUGIN_ID = MANIFEST.name
45
+
46
+ /** Platform module table (must stay aligned with packages/client/web/src/platform.ts + the runtime exemption). */
47
+ const EXTERNALS = [
48
+ 'react',
49
+ 'react/jsx-runtime',
50
+ 'react-dom',
51
+ 'react-dom/client',
52
+ '@deepseek-ai/cordis',
53
+ '@deepseek-ai/dsh-client-ui-slots',
54
+ '@deepseek-ai/dsh-client-web-react',
55
+ '@deepseek-ai/dsh-client-ui-primitives',
56
+ '@deepseek-ai/dsh-client-ui-attachment',
57
+ '@deepseek-ai/dsh-client-schema-form',
58
+ '@deepseek-ai/dsh-client-runtime/client',
59
+ ]
60
+
61
+ /** Locate the esbuild package inside a pnpm checkout (store or hoisted). */
62
+ function resolveEsbuild(checkout) {
63
+ const store = join(checkout, 'node_modules/.pnpm')
64
+ if (existsSync(store)) {
65
+ const entries = readdirSync(store).filter((name) => name.startsWith('esbuild@')).sort()
66
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
67
+ const candidate = join(store, entries[i], 'node_modules/esbuild/package.json')
68
+ if (existsSync(candidate)) return candidate
69
+ }
70
+ }
71
+ const hoisted = join(checkout, 'node_modules/esbuild/package.json')
72
+ if (existsSync(hoisted)) return hoisted
73
+ throw new Error(`esbuild not found under ${checkout} (set DSH_SOURCE to the DSH checkout root)`)
74
+ }
75
+
76
+ const require = createRequire(resolveEsbuild(CHECKOUT))
77
+ const esbuild = require('esbuild')
78
+
79
+ /** Identity class map for one CSS module + the stylesheet text, as a JS module. */
80
+ function cssModuleLoader() {
81
+ return {
82
+ name: 'dsh-mcp-css-modules',
83
+ setup(build) {
84
+ build.onLoad({ filter: /\.module\.css$/ }, (args) => {
85
+ const css = readFileSync(args.path, 'utf8')
86
+ const classes = [...new Set(
87
+ [...css.matchAll(/\.([A-Za-z_][A-Za-z0-9_-]*)/g)].map((match) => match[1]),
88
+ )]
89
+ const map = Object.fromEntries(classes.map((name) => [name, name]))
90
+ const file = args.path.split('/').pop()
91
+ const contents = [
92
+ `(function () {`,
93
+ ` if (typeof document !== 'undefined') {`,
94
+ ` const existing = document.querySelector('style[data-plugin="${PLUGIN_ID}"][data-file="${file}"]');`,
95
+ ` if (!existing) {`,
96
+ ` const style = document.createElement('style');`,
97
+ ` style.setAttribute('data-plugin', '${PLUGIN_ID}');`,
98
+ ` style.setAttribute('data-file', '${file}');`,
99
+ ` style.textContent = ${JSON.stringify(css)};`,
100
+ ` document.head.appendChild(style);`,
101
+ ` }`,
102
+ ` }`,
103
+ `})();`,
104
+ `export default ${JSON.stringify(map)};`,
105
+ ].join('\n')
106
+ return { contents, loader: 'js' }
107
+ })
108
+ },
109
+ }
110
+ }
111
+
112
+ const banner = [
113
+ `window.__ModuleLoader__.load({ id: ${JSON.stringify(PLUGIN_ID)}, factory: (require) => {`,
114
+ 'var module = { exports: {} }; var exports = module.exports;',
115
+ ].join('\n')
116
+ const footer = 'return module.exports; } });'
117
+
118
+ await esbuild.build({
119
+ entryPoints: [join(ROOT, 'src/client/index.ts')],
120
+ outfile: join(ROOT, 'lib/client.js'),
121
+ bundle: true,
122
+ format: 'cjs',
123
+ platform: 'browser',
124
+ target: 'es2022',
125
+ // Components import only named react hooks (React 17+ style); the classic
126
+ // 'transform' mode emits bare `React.createElement` with no React binding in
127
+ // scope, which crashed the settings section with "React is not defined".
128
+ // 'automatic' emits jsx-runtime calls (`react/jsx-runtime` is already an
129
+ // external platform module), so no default React import is required.
130
+ jsx: 'automatic',
131
+ external: EXTERNALS,
132
+ // The plugin lives outside any node_modules tree of its own; the DSH
133
+ // profiles fallback directory is where zod (inlined into the bundle) and the
134
+ // external @deepseek-ai/* platform modules resolve from at build time.
135
+ nodePaths: [join(homedir(), '.dsh/profiles/node_modules')],
136
+ plugins: [cssModuleLoader()],
137
+ define: {
138
+ 'process.env.NODE_ENV': '"production"',
139
+ },
140
+ banner: { js: banner },
141
+ footer: { js: footer },
142
+ })
143
+
144
+ console.log('lib/client.js built')
@@ -0,0 +1,236 @@
1
+ .section {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 14px;
5
+ width: 100%;
6
+ max-width: 760px;
7
+ color: var(--dsw-alias-label-primary);
8
+ }
9
+
10
+ .header {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: flex-end;
14
+ }
15
+
16
+ .status,
17
+ .failure p {
18
+ margin: 0;
19
+ }
20
+
21
+ .status,
22
+ .failure {
23
+ font-size: 13px;
24
+ line-height: 20px;
25
+ color: var(--dsw-alias-label-tertiary);
26
+ }
27
+
28
+ .failure {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 10px;
32
+ color: var(--dsw-alias-state-error-primary);
33
+ }
34
+
35
+ button {
36
+ border: 1px solid var(--dsw-alias-border-l2);
37
+ border-radius: 6px;
38
+ padding: 5px 12px;
39
+ background: transparent;
40
+ color: var(--dsw-alias-label-primary);
41
+ font: inherit;
42
+ font-size: 13px;
43
+ line-height: 20px;
44
+ cursor: pointer;
45
+ }
46
+
47
+ button:hover {
48
+ background: var(--dsw-alias-interactive-bg-hover);
49
+ }
50
+
51
+ button:focus-visible {
52
+ outline: 2px solid var(--dsw-alias-state-business-primary);
53
+ outline-offset: 1px;
54
+ }
55
+
56
+ button:disabled {
57
+ cursor: not-allowed;
58
+ opacity: 0.55;
59
+ }
60
+
61
+ .primary {
62
+ border-color: var(--dsw-alias-state-business-primary);
63
+ background: var(--dsw-alias-state-business-primary);
64
+ color: var(--dsw-alias-label-on-accent);
65
+ }
66
+
67
+ .primary:hover {
68
+ background: var(--dsw-alias-state-business-primary-hover);
69
+ }
70
+
71
+ .list {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 8px;
75
+ margin: 0;
76
+ padding: 0;
77
+ list-style: none;
78
+ }
79
+
80
+ .row {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ gap: 12px;
85
+ border: 1px solid var(--dsw-alias-border-l2);
86
+ border-radius: 10px;
87
+ padding: 10px 14px;
88
+ background: var(--dsw-alias-bg-layer-3);
89
+ }
90
+
91
+ .rowMain {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 4px;
95
+ min-width: 0;
96
+ }
97
+
98
+ .rowTitle {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 8px;
102
+ min-width: 0;
103
+ }
104
+
105
+ .serverName {
106
+ overflow: hidden;
107
+ font-size: 14px;
108
+ line-height: 20px;
109
+ font-weight: 600;
110
+ text-overflow: ellipsis;
111
+ white-space: nowrap;
112
+ }
113
+
114
+ .badge {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ min-height: 18px;
118
+ border-radius: 5px;
119
+ padding: 1px 6px;
120
+ font-size: 11px;
121
+ line-height: 16px;
122
+ white-space: nowrap;
123
+ }
124
+
125
+ .badge.mounting {
126
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);
127
+ color: var(--dsw-alias-state-business-primary);
128
+ }
129
+
130
+ .badge.live {
131
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);
132
+ color: var(--dsw-alias-state-success-primary);
133
+ }
134
+
135
+ .badge.failed {
136
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
137
+ color: var(--dsw-alias-state-error-primary);
138
+ }
139
+
140
+ .badge.stopped {
141
+ background: var(--dsw-alias-bg-layer-1);
142
+ color: var(--dsw-alias-label-tertiary);
143
+ }
144
+
145
+ .muted {
146
+ color: var(--dsw-alias-label-tertiary);
147
+ }
148
+
149
+ .rowMeta {
150
+ display: flex;
151
+ gap: 14px;
152
+ color: var(--dsw-alias-label-tertiary);
153
+ font-size: 12px;
154
+ line-height: 18px;
155
+ font-variant-numeric: tabular-nums;
156
+ }
157
+
158
+ .rowActions {
159
+ flex: none;
160
+ }
161
+
162
+ .card {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 0;
166
+ border: 1px solid var(--dsw-alias-border-l2);
167
+ border-radius: 10px;
168
+ background: var(--dsw-alias-bg-layer-3);
169
+ }
170
+
171
+ .card > .row {
172
+ border: 0;
173
+ border-radius: 0;
174
+ background: transparent;
175
+ }
176
+
177
+ .modeRow {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 18px;
181
+ flex-wrap: wrap;
182
+ border: 1px solid var(--dsw-alias-border-l2);
183
+ border-radius: 10px;
184
+ padding: 8px 14px;
185
+ background: var(--dsw-alias-bg-layer-3);
186
+ font-size: 13px;
187
+ line-height: 20px;
188
+ }
189
+
190
+ .modeLabel {
191
+ font-weight: 600;
192
+ }
193
+
194
+ .modeOption {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 6px;
198
+ cursor: pointer;
199
+ }
200
+
201
+ .modeHint {
202
+ margin: 0;
203
+ color: var(--dsw-alias-label-tertiary);
204
+ font-size: 12px;
205
+ line-height: 18px;
206
+ }
207
+
208
+ .toolPanel {
209
+ border-top: 1px solid var(--dsw-alias-border-l2);
210
+ padding: 10px 14px;
211
+ }
212
+
213
+ .toolGrid {
214
+ display: grid;
215
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
216
+ gap: 4px 14px;
217
+ max-height: 320px;
218
+ overflow-y: auto;
219
+ }
220
+
221
+ .toolRow {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 8px;
225
+ min-width: 0;
226
+ padding: 2px 0;
227
+ font-size: 12px;
228
+ line-height: 18px;
229
+ cursor: pointer;
230
+ }
231
+
232
+ .toolName {
233
+ overflow: hidden;
234
+ text-overflow: ellipsis;
235
+ white-space: nowrap;
236
+ }