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 +5 -0
- package/cordis.patch.yml +4 -0
- package/lib/index.js +127 -0
- package/package.json +32 -0
package/README.md
ADDED
package/cordis.patch.yml
ADDED
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
|
+
}
|