@sunt2049/dsh-mcp-manager 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +57 -2
- package/client.js +1289 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +8 -0
- package/index.js +960 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +53 -4
- package/patch-doc.js +287 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunt2049/dsh-mcp-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP 控制台:一个 profile 里所有 MCP 服务器的总览、实时工具清单与完整 JSON Schema、连通性探测,以及增删/停用与配置编辑。",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"cordis",
|
|
11
|
+
"mcp",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"mcp-client",
|
|
14
|
+
"plugin"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "sunt2049",
|
|
18
|
+
"icon": "./icon.svg",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./index.js",
|
|
21
|
+
"./client": "./client.js",
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./locale/*.json": "./locale/*.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"client.js",
|
|
28
|
+
"patch-doc.js",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"icon.svg",
|
|
31
|
+
"locale/*.json",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"inject": [],
|
|
41
|
+
"platform": "web"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0-0",
|
|
46
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/patch-doc.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-preserving reader/writer for a profile `cordis.patch.yml`.
|
|
3
|
+
*
|
|
4
|
+
* The document is modelled as ordered blocks so that comments, blank lines and
|
|
5
|
+
* unrelated entries are re-emitted verbatim; only the MCP rows this console
|
|
6
|
+
* owns are ever rewritten. `serializePatch(parsePatch(text)) === text` is the
|
|
7
|
+
* invariant the caller verifies before writing anything.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ROW_START = /^ {4}- /
|
|
11
|
+
const TOP_ITEM = /^- /
|
|
12
|
+
const INSERT_HEADER = /^- insert:\s*$/
|
|
13
|
+
|
|
14
|
+
function idOf(line) {
|
|
15
|
+
const match = /^\s*-\s*id:\s*(.+?)\s*$/.exec(line)
|
|
16
|
+
if (!match) return ''
|
|
17
|
+
return match[1].replace(/^['"]|['"]$/g, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parsePatch(source) {
|
|
21
|
+
const lines = String(source).replace(/\r\n/g, '\n').split('\n')
|
|
22
|
+
const blocks = []
|
|
23
|
+
let index = 0
|
|
24
|
+
|
|
25
|
+
while (index < lines.length) {
|
|
26
|
+
const line = lines[index]
|
|
27
|
+
|
|
28
|
+
if (INSERT_HEADER.test(line)) {
|
|
29
|
+
const header = line
|
|
30
|
+
index += 1
|
|
31
|
+
const rows = []
|
|
32
|
+
while (index < lines.length) {
|
|
33
|
+
if (ROW_START.test(lines[index])) {
|
|
34
|
+
const start = index
|
|
35
|
+
let end = start + 1
|
|
36
|
+
while (end < lines.length) {
|
|
37
|
+
const next = lines[end]
|
|
38
|
+
if (ROW_START.test(next)) break
|
|
39
|
+
if (/^\s*$/.test(next) || /^\s{4,}/.test(next)) {
|
|
40
|
+
end += 1
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
let rowEnd = end
|
|
46
|
+
while (rowEnd > start + 1 && /^\s*$/.test(lines[rowEnd - 1])) rowEnd -= 1
|
|
47
|
+
rows.push({
|
|
48
|
+
id: idOf(lines[start]),
|
|
49
|
+
lines: lines.slice(start, rowEnd),
|
|
50
|
+
after: lines.slice(rowEnd, end),
|
|
51
|
+
})
|
|
52
|
+
index = end
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
if (/^\s*$/.test(lines[index])) {
|
|
56
|
+
index += 1
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
blocks.push({ kind: 'insert', header, rows })
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (TOP_ITEM.test(line)) {
|
|
66
|
+
const start = index
|
|
67
|
+
let end = start + 1
|
|
68
|
+
while (end < lines.length && !TOP_ITEM.test(lines[end])) end += 1
|
|
69
|
+
blocks.push({
|
|
70
|
+
kind: 'entry',
|
|
71
|
+
rows: [{ id: idOf(lines[start]), lines: lines.slice(start, end), after: [] }],
|
|
72
|
+
})
|
|
73
|
+
index = end
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const start = index
|
|
78
|
+
let end = start
|
|
79
|
+
while (end < lines.length && !TOP_ITEM.test(lines[end])) end += 1
|
|
80
|
+
blocks.push({ kind: 'text', lines: lines.slice(start, end) })
|
|
81
|
+
index = end
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return blocks
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function serializePatch(blocks) {
|
|
88
|
+
const out = []
|
|
89
|
+
for (const block of blocks) {
|
|
90
|
+
if (block.kind === 'text') {
|
|
91
|
+
out.push(...block.lines)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (block.kind === 'insert') {
|
|
95
|
+
if (Array.isArray(block.before)) out.push(...block.before)
|
|
96
|
+
out.push(block.header)
|
|
97
|
+
for (const row of block.rows) out.push(...row.lines, ...row.after)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
out.push(...block.rows[0].lines)
|
|
101
|
+
}
|
|
102
|
+
return out.join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Remove one row; an insert group left with no rows is dropped with it.
|
|
106
|
+
* The neighbouring row ids come back with it so a later re-insert can land
|
|
107
|
+
* where the row used to be instead of at the end of the document. */
|
|
108
|
+
export function removeRow(blocks, id) {
|
|
109
|
+
const order = []
|
|
110
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
111
|
+
if (block.kind === 'text') continue
|
|
112
|
+
for (const [rowIndex, row] of block.rows.entries()) order.push({ id: row.id, blockIndex, rowIndex })
|
|
113
|
+
}
|
|
114
|
+
const at = order.findIndex((entry) => entry.id === id)
|
|
115
|
+
if (at === -1) return { blocks, removed: null, beforeId: null, afterId: null }
|
|
116
|
+
|
|
117
|
+
const beforeId = at > 0 ? order[at - 1].id : null
|
|
118
|
+
const afterId = at < order.length - 1 ? order[at + 1].id : null
|
|
119
|
+
|
|
120
|
+
const kept = []
|
|
121
|
+
let removed = null
|
|
122
|
+
|
|
123
|
+
for (const block of blocks) {
|
|
124
|
+
if (block.kind === 'text') {
|
|
125
|
+
kept.push(block)
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (block.kind === 'entry') {
|
|
129
|
+
if (block.rows[0].id === id) {
|
|
130
|
+
removed = block.rows[0].lines.join('\n')
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
kept.push(block)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const hit = block.rows.find((row) => row.id === id)
|
|
137
|
+
if (!hit) {
|
|
138
|
+
kept.push(block)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
removed = hit.lines.join('\n')
|
|
142
|
+
const rows = block.rows.filter((row) => row.id !== id)
|
|
143
|
+
if (rows.length > 0) kept.push({ ...block, rows })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { blocks: kept, removed, beforeId, afterId }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Put a row back next to a remembered neighbour. Falls back to appending when
|
|
151
|
+
* both anchors are gone, so a stale record can never be lost.
|
|
152
|
+
*
|
|
153
|
+
* The row keeps the shape it was stored in: a bare `- id:` line goes back as a
|
|
154
|
+
* bare entry, an indented one goes back inside an insert group. Splicing an
|
|
155
|
+
* indented line into a top-level entry (or the other way round) would produce
|
|
156
|
+
* invalid YAML, so the anchor's kind never decides the row's kind.
|
|
157
|
+
*/
|
|
158
|
+
export function insertRow(blocks, lines, anchors = {}) {
|
|
159
|
+
const row = { id: idOf(lines[0]), lines, after: [] }
|
|
160
|
+
const bare = /^- /.test(lines[0])
|
|
161
|
+
const makeBlock = () =>
|
|
162
|
+
bare
|
|
163
|
+
? { kind: 'entry', rows: [row] }
|
|
164
|
+
: { kind: 'insert', before: [], header: '- insert:', rows: [row] }
|
|
165
|
+
|
|
166
|
+
const locate = (wanted) => {
|
|
167
|
+
if (!wanted) return null
|
|
168
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
169
|
+
if (block.kind === 'text') continue
|
|
170
|
+
const rowIndex = block.rows.findIndex((item) => item.id === wanted)
|
|
171
|
+
if (rowIndex !== -1) return { blockIndex, block, rowIndex }
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// `beforeId` is the predecessor, so the row goes right after it; `afterId` is
|
|
177
|
+
// the successor, so the row goes right before it.
|
|
178
|
+
const previous = locate(anchors.beforeId)
|
|
179
|
+
if (previous) {
|
|
180
|
+
const out = [...blocks]
|
|
181
|
+
if (previous.block.kind === 'insert' && !bare) {
|
|
182
|
+
const rows = [...previous.block.rows]
|
|
183
|
+
rows.splice(previous.rowIndex + 1, 0, row)
|
|
184
|
+
out[previous.blockIndex] = { ...previous.block, rows }
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
out.splice(previous.blockIndex + 1, 0, makeBlock())
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const next = locate(anchors.afterId)
|
|
192
|
+
if (next) {
|
|
193
|
+
const out = [...blocks]
|
|
194
|
+
if (next.block.kind === 'insert' && !bare) {
|
|
195
|
+
const rows = [...next.block.rows]
|
|
196
|
+
rows.splice(next.rowIndex, 0, row)
|
|
197
|
+
out[next.blockIndex] = { ...next.block, rows }
|
|
198
|
+
return out
|
|
199
|
+
}
|
|
200
|
+
out.splice(next.blockIndex, 0, makeBlock())
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return appendRow(blocks, lines)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Append a row as its own insert group at the end of the document. */
|
|
208
|
+
export function appendRow(blocks, lines) {
|
|
209
|
+
const next = [...blocks]
|
|
210
|
+
const tail = serializePatch(next)
|
|
211
|
+
const before = tail.endsWith('\n') || tail === '' ? [] : ['']
|
|
212
|
+
next.push({
|
|
213
|
+
kind: 'insert',
|
|
214
|
+
before,
|
|
215
|
+
header: '- insert:',
|
|
216
|
+
rows: [{ id: idOf(lines[0]), lines, after: [] }],
|
|
217
|
+
})
|
|
218
|
+
return next
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function rowIds(blocks) {
|
|
222
|
+
const ids = []
|
|
223
|
+
for (const block of blocks) {
|
|
224
|
+
if (block.kind === 'text') continue
|
|
225
|
+
for (const row of block.rows) if (row.id) ids.push(row.id)
|
|
226
|
+
}
|
|
227
|
+
return ids
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function quote(value) {
|
|
231
|
+
return `'${String(value).replace(/'/g, "''")}'`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Build the YAML lines of one MCP client row, at insert-group indentation. */
|
|
235
|
+
export function buildRowLines(id, config) {
|
|
236
|
+
const lines = [` - id: ${id}`, " name: '@deepseek-ai/dsh-mcp-client'", ' config:']
|
|
237
|
+
const field = (key, value) => lines.push(` ${key}: ${value}`)
|
|
238
|
+
|
|
239
|
+
field('serverName', quote(config.serverName))
|
|
240
|
+
field('transport', quote(config.transport))
|
|
241
|
+
|
|
242
|
+
if (config.transport === 'streamable-http') {
|
|
243
|
+
field('url', quote(config.url))
|
|
244
|
+
const headers = config.headers && typeof config.headers === 'object' ? config.headers : {}
|
|
245
|
+
const names = Object.keys(headers)
|
|
246
|
+
if (names.length > 0) {
|
|
247
|
+
lines.push(' headers:')
|
|
248
|
+
for (const name of names) lines.push(` ${name}: ${quote(headers[name])}`)
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
field('command', quote(config.command))
|
|
252
|
+
const args = Array.isArray(config.args) ? config.args : []
|
|
253
|
+
if (args.length > 0) {
|
|
254
|
+
lines.push(' args:')
|
|
255
|
+
for (const arg of args) lines.push(` - ${quote(arg)}`)
|
|
256
|
+
}
|
|
257
|
+
const env = config.env && typeof config.env === 'object' ? config.env : {}
|
|
258
|
+
const keys = Object.keys(env)
|
|
259
|
+
if (keys.length > 0) {
|
|
260
|
+
lines.push(' env:')
|
|
261
|
+
for (const key of keys) lines.push(` ${key}: ${quote(env[key])}`)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof config.toolCallTimeoutMs === 'number' && config.toolCallTimeoutMs > 0) {
|
|
266
|
+
field('toolCallTimeoutMs', String(config.toolCallTimeoutMs))
|
|
267
|
+
}
|
|
268
|
+
if (config.failOnStartupError === true) field('failOnStartupError', 'true')
|
|
269
|
+
|
|
270
|
+
return lines
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Read the shape of a row out of its YAML lines. */
|
|
274
|
+
export function describeRow(lines) {
|
|
275
|
+
const text = Array.isArray(lines) ? lines.join('\n') : String(lines || '')
|
|
276
|
+
const pick = (key) => {
|
|
277
|
+
const match = new RegExp(`^\\s+${key}:\\s*(.+?)\\s*$`, 'm').exec(text)
|
|
278
|
+
return match ? match[1].replace(/^['"]|['"]$/g, '') : ''
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
name: pick('name'),
|
|
282
|
+
serverName: pick('serverName'),
|
|
283
|
+
transport: pick('transport'),
|
|
284
|
+
url: pick('url'),
|
|
285
|
+
command: pick('command'),
|
|
286
|
+
}
|
|
287
|
+
}
|