@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/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "MCP Console",
4
+ "description": "Lists the profile's MCP servers with a live per-server tool inventory, full JSON Schemas, and direct edits of the active config entries through configEditor."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "MCP 控制台",
4
+ "description": "列出 profile 里的 MCP 服务器,按服务器展开实时工具清单与完整 JSON Schema,并可经 configEditor 直接修改活跃配置条目。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@sunt2049/dsh-mcp-manager",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }