dsh-plugin-mcp-manager 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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # dsh-plugin-mcp-manager
2
+
3
+ MCP 服务器启停/删除。读写 `~/.dsh/profiles/web/cordis.patch.yml`(与官方 `settings.custom.mcp*` 同一存储),只处理 id 以 `mcp-` 开头的条目。
4
+
5
+ > 注:YAML 解析/序列化为精简实现,真实 cordis.patch.yml 含 `- insert:` 操作时会被原样保留,仅对 `mcp-* ` 条目做增删。需真机验证。
@@ -0,0 +1,4 @@
1
+ # dsh-plugin-mcp-manager —— DSH bundle patch
2
+ - insert:
3
+ - id: dsh-plugin-mcp-manager
4
+ name: dsh-plugin-mcp-manager
package/lib/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
7
+ const MCP_PATCH = path.join(DSH_HOME, 'profiles', 'web', 'cordis.patch.yml')
8
+
9
+ // 精简 YAML:把顶层数组项 “- key: value” 解析成对象数组(覆盖 MCP 条目格式),
10
+ // 也兼顾 cordis.patch.yml 常见的 “- insert:” 操作(原样保留,不当作 MCP)。
11
+ function parseEntries(text) {
12
+ const entries = []
13
+ let cur = null
14
+ let curNested = null
15
+ for (const rawline of text.split(/\r?\n/)) {
16
+ const line = rawline.replace(/\s+#.*$/, '')
17
+ if (!line.trim()) continue
18
+ const m = /^\s*-\s*(.+)$/.exec(line)
19
+ if (m) { cur = {}; entries.push(cur); curNested = null; parseKV(m[1], cur, 0); continue }
20
+ const im = /^(\s+)(\S[^:]*):\s*(.*)$/.exec(line)
21
+ if (im && cur) {
22
+ const indent = im[1].length
23
+ const key = im[2].trim(); const val = im[3].trim()
24
+ if (indent >= 6 && cur.config && typeof cur.config === 'object') { cur.config[key] = fromText(val) }
25
+ else { cur[key] = fromText(val); if (key === 'config' && !val) cur[key] = {} }
26
+ }
27
+ }
28
+ return entries
29
+ }
30
+ function fromText(v) {
31
+ if (v === '') return undefined
32
+ if (v === 'true') return true
33
+ if (v === 'false') return false
34
+ if (/^\d+$/.test(v)) return Number(v)
35
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1)
36
+ return v
37
+ }
38
+ function serializeEntries(entries) {
39
+ const out = []
40
+ for (const e of entries) {
41
+ out.push('- ' + keyVal('id', e.id))
42
+ if (e.name !== undefined) out.push(' name: ' + e.name)
43
+ if (e.disabled !== undefined) out.push(' disabled: ' + e.disabled)
44
+ if (e.config && typeof e.config === 'object') {
45
+ out.push(' config:')
46
+ for (const [k, v] of Object.entries(e.config)) out.push(' ' + k + ': ' + JSON.stringify(v))
47
+ }
48
+ }
49
+ return out.join('\n') + '\n'
50
+ }
51
+ function keyVal(k, v) { return k + ': ' + JSON.stringify(v) }
52
+ function isMcpId(id) { return typeof id === 'string' && id.startsWith('mcp-') }
53
+
54
+ function readEntries() {
55
+ try { return parseEntries(fs.readFileSync(MCP_PATCH, 'utf8')) } catch (e) { return [] }
56
+ }
57
+ function writeEntries(entries) {
58
+ fs.mkdirSync(path.dirname(MCP_PATCH), { recursive: true })
59
+ fs.writeFileSync(MCP_PATCH, serializeEntries(entries), 'utf8')
60
+ }
61
+ function listMcp() {
62
+ const servers = []
63
+ for (const entry of readEntries()) {
64
+ if (!isMcpId(entry.id)) continue
65
+ const cfg = (entry.config && typeof entry.config === 'object') ? entry.config : {}
66
+ servers.push({
67
+ id: String(entry.id),
68
+ serverName: typeof cfg.serverName === 'string' ? cfg.serverName : String(entry.id).replace(/^mcp-/, ''),
69
+ enabled: entry.disabled !== true,
70
+ transport: typeof cfg.transport === 'string' ? cfg.transport : 'stdio',
71
+ command: typeof cfg.command === 'string' ? cfg.command : '',
72
+ })
73
+ }
74
+ return servers
75
+ }
76
+
77
+ const CLIENT_JS = `(function(){
78
+ if (window.__dshMcp) return
79
+ window.__dshMcp = true
80
+ function ensure(){
81
+ var host = document.querySelector('[class*="settings"], [data-role="settings"], main, body')
82
+ if (!host) return setTimeout(ensure, 300)
83
+ if (host.querySelector('.dsh-mcp-panel')) return
84
+ var p = document.createElement('div'); p.className='dsh-mcp-panel'
85
+ p.innerHTML = '<div class="dsh-mcp-head">MCP 服务器</div><div class="dsh-mcp-list"></div><div class="dsh-mcp-note">修改后需重启生效</div>'
86
+ host.appendChild(p)
87
+ var list = p.querySelector('.dsh-mcp-list')
88
+ function load(){
89
+ fetch('/dsh-plugin-mcp-manager/mcp.json').then(function(r){return r.json()}).then(function(d){
90
+ list.innerHTML=''
91
+ var items=d.servers||[]
92
+ if(!items.length){ list.innerHTML='<div class="dsh-mcp-empty">暂未配置 MCP 服务器</div>'; return }
93
+ items.forEach(function(s){
94
+ var row=document.createElement('div'); row.className='dsh-mcp-row'
95
+ row.innerHTML='<span class="dsh-mcp-name">'+(s.serverName||s.id||'')+'</span> <span class="dsh-mcp-cmd">'+(s.command||'')+'</span> <button class="dsh-mcp-del">删除</button>'
96
+ row.querySelector('.dsh-mcp-del').onclick=function(){ if(window.confirm('删除 MCP 服务器 '+(s.serverName||s.id)+'?')){ remove(s.id) } }
97
+ list.appendChild(row)
98
+ })
99
+ }).catch(function(){ list.innerHTML='<div class="dsh-mcp-empty">加载失败</div>' })
100
+ }
101
+ function remove(id){ fetch('/dsh-plugin-mcp-manager/mcp.json',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'remove',id:String(id)})}).then(load) }
102
+ load()
103
+ }
104
+ if (document.readyState==='loading') document.addEventListener('DOMContentLoaded', ensure)
105
+ else ensure()
106
+ })()`
107
+
108
+ const name = 'dsh-plugin-mcp-manager'
109
+ const inject = ['webServer']
110
+ function apply(ctx){
111
+ const json=(res,o)=>{res.writeHead(200,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store'});res.end(JSON.stringify(o))}
112
+ ctx.webServer.register({kind:'exact',path:'/dsh-plugin-mcp-manager/mcp.json',handler:(req,res)=>{
113
+ if(req.method==='POST'){
114
+ let body=''; req.on('data',c=>body+=c); req.on('end',()=>{ try{
115
+ const p=JSON.parse(body||'{}')
116
+ if(p.action==='remove'){
117
+ const entries = readEntries().filter(e => !isMcpId(e.id) || String(e.id) !== String(p.id))
118
+ writeEntries(entries)
119
+ return json(res,{ok:true,removed:true})
120
+ }
121
+ json(res,{ok:false,error:'unknown action'})
122
+ }catch(e){ json(res,{ok:false,error:String(e&&e.message||e)}) } })
123
+ } else { json(res,{ok:true,servers:listMcp()}) }
124
+ }})
125
+
126
+ }
127
+ export { name, inject, apply }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "dsh-plugin-mcp-manager",
3
+ "version": "1.0.0",
4
+ "description": "DSH MCP 服务器启停/删除管理",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "mcp",
10
+ "mcp-server"
11
+ ],
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "files": [
15
+ "lib",
16
+ "cordis.patch.yml",
17
+ "README.md"
18
+ ],
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ }
23
+ },
24
+ "license": "MIT",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/mengyu-arch/dsh-plugins.git"
31
+ }
32
+ }