@xxxyz/dsh-mcp-manager 2.1.1 → 2.1.3
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 +3 -0
- package/README_EN.md +3 -0
- package/lib/client.js +44 -4
- package/lib/index.js +102 -21
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ dsh plugin --profile web remove @xxxyz/dsh-mcp-manager
|
|
|
126
126
|
| 字段 | 说明 |
|
|
127
127
|
|---|---|
|
|
128
128
|
| `version` | loader 行 `config.version`,仅用于触发 HMR 重应用;官方通道安装下由 bundle 自动管理,无需手动修改。 |
|
|
129
|
+
| `token` | **可选**访问令牌(写操作鉴权,纵深防御)。设置后写操作(增删改/启停/重启/导入导出/技能停用)须带 `x-dsh-token: <token>` 头;设置页提供令牌输入框(保存在浏览器 localStorage)。也可用环境变量 `DSH_MCP_MANAGER_TOKEN` 配置。默认关闭。 |
|
|
129
130
|
|
|
130
131
|
loader 行必须为 **`insert` 块**形式(DSH patch 方言中普通 `- id:` 行只是对已存在条目的覆盖,无法新增插件):
|
|
131
132
|
|
|
@@ -133,6 +134,8 @@ loader 行必须为 **`insert` 块**形式(DSH patch 方言中普通 `- id:`
|
|
|
133
134
|
- insert:
|
|
134
135
|
- id: dsh-mcp-manager
|
|
135
136
|
name: '@xxxyz/dsh-mcp-manager'
|
|
137
|
+
config:
|
|
138
|
+
token: 你的访问令牌 # 可选:开启写操作鉴权
|
|
136
139
|
```
|
|
137
140
|
|
|
138
141
|
> 无需手动写这行——`dsh plugin add` 的 bundle patch 会自动插入(见 `cordis.patch.yml`)。
|
package/README_EN.md
CHANGED
|
@@ -127,6 +127,7 @@ Configuration of the plugin itself on its loader row:
|
|
|
127
127
|
| Field | Description |
|
|
128
128
|
|---|---|
|
|
129
129
|
| `version` | The loader row's `config.version`, only used to trigger an HMR re-apply; auto-managed by the bundle channel — no manual edits needed. |
|
|
130
|
+
| `token` | **Optional** access token (write-op auth, defense in depth). When set, state-changing ops (add/edit/remove/enable/restart/import/export/skill-toggle) require the `x-dsh-token: <token>` header; the settings pages provide a token input (stored in browser localStorage). Can also be set via the `DSH_MCP_MANAGER_TOKEN` env var. Off by default. |
|
|
130
131
|
|
|
131
132
|
The loader row must be an **`insert` block** (in DSH's patch dialect a plain `- id:` row only overrides existing entries and can never add a new plugin):
|
|
132
133
|
|
|
@@ -134,6 +135,8 @@ The loader row must be an **`insert` block** (in DSH's patch dialect a plain `-
|
|
|
134
135
|
- insert:
|
|
135
136
|
- id: dsh-mcp-manager
|
|
136
137
|
name: '@xxxyz/dsh-mcp-manager'
|
|
138
|
+
config:
|
|
139
|
+
token: your-access-token # optional: enable write-op auth
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
> You don't need to write this row manually — `dsh plugin add` inserts it automatically via the bundle patch (see `cordis.patch.yml`).
|
package/lib/client.js
CHANGED
|
@@ -50,7 +50,10 @@ const factory = (require) => {
|
|
|
50
50
|
'.mcpm-dialog-actions{display:flex;justify-content:flex-end;gap:8px}' +
|
|
51
51
|
'.skm-search{font-size:12px;padding:5px 8px;border-radius:6px;border:1px solid rgba(128,128,128,.5);background:transparent;color:inherit;width:100%;box-sizing:border-box}' +
|
|
52
52
|
'.skm-group-title{font-size:12px;font-weight:600;opacity:.85;margin-top:4px;padding-bottom:2px;border-bottom:1px solid rgba(128,128,128,.25)}' +
|
|
53
|
-
'.skm-provider-title{font-size:12px;font-weight:600;cursor:pointer;user-select:none;margin-top:4px;opacity:.9}'
|
|
53
|
+
'.skm-provider-title{font-size:12px;font-weight:600;cursor:pointer;user-select:none;margin-top:4px;opacity:.9}' +
|
|
54
|
+
'.mcpm-version{margin-left:8px;font-size:11px;font-weight:500;opacity:.55;letter-spacing:.3px;vertical-align:middle}' +
|
|
55
|
+
'.mcpm-token{display:flex;align-items:center;gap:8px;font-size:12px;opacity:.85}' +
|
|
56
|
+
'.mcpm-token input{font-size:12px;padding:3px 6px;border-radius:5px;border:1px solid rgba(128,128,128,.5);background:transparent;color:inherit;flex:1;max-width:260px}'
|
|
54
57
|
|
|
55
58
|
function ensureCss() {
|
|
56
59
|
if (typeof document === 'undefined') return
|
|
@@ -63,17 +66,52 @@ const factory = (require) => {
|
|
|
63
66
|
document.head.appendChild(tag)
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
// Optional access token for write ops (host config.token or
|
|
70
|
+
// DSH_MCP_MANAGER_TOKEN). Kept in localStorage so the user enters it once;
|
|
71
|
+
// sent as `x-dsh-token` on every request (host ignores it when unset).
|
|
72
|
+
let TOKEN = ''
|
|
73
|
+
try { TOKEN = window.localStorage.getItem('dsh-mcp-manager-token') || '' } catch (e) { /* storage unavailable */ }
|
|
74
|
+
function getToken() { return TOKEN }
|
|
75
|
+
function setToken(v) {
|
|
76
|
+
TOKEN = String(v || '').trim()
|
|
77
|
+
try { if (TOKEN) window.localStorage.setItem('dsh-mcp-manager-token', TOKEN); else window.localStorage.removeItem('dsh-mcp-manager-token') } catch (e) { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
function apiCall(op, args) {
|
|
67
81
|
// `x-dsh-plugin` is the cross-site (CSRF) gate header the host half
|
|
68
82
|
// requires on every request; a cross-origin page cannot attach it
|
|
69
83
|
// without a CORS preflight that this route never answers.
|
|
84
|
+
const headers = { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-mcp-manager' }
|
|
85
|
+
if (TOKEN) headers['x-dsh-token'] = TOKEN
|
|
70
86
|
return fetch('/dsh-mcp-manager/api', {
|
|
71
87
|
method: 'POST',
|
|
72
|
-
headers
|
|
88
|
+
headers,
|
|
73
89
|
body: JSON.stringify({ op, args: args || {} }),
|
|
74
90
|
}).then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }))
|
|
75
91
|
}
|
|
76
92
|
|
|
93
|
+
// Small version badge shown next to each settings-page title; reads the
|
|
94
|
+
// package version from the host (plugin-version op) so it always matches
|
|
95
|
+
// the installed release.
|
|
96
|
+
function VersionBadge() {
|
|
97
|
+
const [v, setV] = React.useState(null)
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
let alive = true
|
|
100
|
+
apiCall('plugin-version', {}).then((r) => { if (alive && r && r.ok) setV(r.version) }).catch(() => {})
|
|
101
|
+
return () => { alive = false }
|
|
102
|
+
}, [])
|
|
103
|
+
return v ? React.createElement('span', { className: 'mcpm-version' }, 'v' + v) : null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Optional access-token row: shown on both settings pages; only needed
|
|
107
|
+
// when the host has token auth enabled (write ops would 401 otherwise).
|
|
108
|
+
function TokenRow() {
|
|
109
|
+
const [val, setVal] = React.useState(getToken())
|
|
110
|
+
return React.createElement('label', { className: 'mcpm-token' },
|
|
111
|
+
'访问令牌(可选,写操作鉴权)',
|
|
112
|
+
React.createElement('input', { value: val, onChange: (e) => { setVal(e.target.value); setToken(e.target.value) }, placeholder: '留空 = 不鉴权' }))
|
|
113
|
+
}
|
|
114
|
+
|
|
77
115
|
module.exports = {
|
|
78
116
|
name: 'dsh-mcp-manager-client',
|
|
79
117
|
inject: ['timer'],
|
|
@@ -218,7 +256,8 @@ const factory = (require) => {
|
|
|
218
256
|
}
|
|
219
257
|
|
|
220
258
|
return React.createElement('div', { className: 'mcpm-wrap' },
|
|
221
|
-
React.createElement('h2', null, 'MCP 服务管理'),
|
|
259
|
+
React.createElement('h2', null, 'MCP 服务管理', React.createElement(VersionBadge, null)),
|
|
260
|
+
React.createElement(TokenRow, null),
|
|
222
261
|
React.createElement('div', { className: 'mcpm-sub' },
|
|
223
262
|
'管理 dsh-mcp-client 服务:写入 项目级(web profile) / 全局(home) 的 cordis.patch.yml,经 HMR 实时生效;重启 DSH 后由 Loader 自动加载。'),
|
|
224
263
|
restartInfo && React.createElement('div', { className: 'mcpm-msg info' },
|
|
@@ -340,7 +379,8 @@ const factory = (require) => {
|
|
|
340
379
|
const query = q.trim().toLowerCase()
|
|
341
380
|
const visible = state.skills.filter((s) => !query || (s.name + ' ' + (s.description || '')).toLowerCase().includes(query))
|
|
342
381
|
return React.createElement('div', { className: 'mcpm-wrap' },
|
|
343
|
-
React.createElement('h2', null, 'Skills 管理'),
|
|
382
|
+
React.createElement('h2', null, 'Skills 管理', React.createElement(VersionBadge, null)),
|
|
383
|
+
React.createElement(TokenRow, null),
|
|
344
384
|
React.createElement('div', { className: 'mcpm-sub' }, '查看与启停 DSH 技能(按层级分组,禁用即时生效,无需重启)'),
|
|
345
385
|
msg && React.createElement('div', { className: 'mcpm-msg ' + msg.kind }, msg.text),
|
|
346
386
|
React.createElement('input', { className: 'skm-search', placeholder: '搜索技能名称或描述…', value: q, onChange: (e) => setQ(e.target.value) }),
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Build: `tsc -p tsconfig.json` compiles this to lib/index.js (the shipped
|
|
12
12
|
// artifact — same convention as DSH's own packages, which ship compiled JS).
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
14
15
|
export default {
|
|
15
16
|
name: 'dsh-mcp-manager-host',
|
|
16
17
|
inject: ['timer', 'fs', 'settings', 'sandboxPolicy', 'webServer', 'tools', 'skills'],
|
|
@@ -22,6 +23,24 @@ export default {
|
|
|
22
23
|
const tools = ctx.tools;
|
|
23
24
|
// pluginInventory is optional: probe at use time, degrade to no live info.
|
|
24
25
|
const pluginInventory = ctx.get('pluginInventory');
|
|
26
|
+
// Package version, surfaced in the Settings pages and the HTTP API. Read
|
|
27
|
+
// from the installed package.json so it always matches the release tag.
|
|
28
|
+
let PKG_VERSION = 'unknown';
|
|
29
|
+
try {
|
|
30
|
+
PKG_VERSION = createRequire(import.meta.url)('../package.json').version || 'unknown';
|
|
31
|
+
}
|
|
32
|
+
catch (e) { /* keep unknown */ }
|
|
33
|
+
// Optional access token (defense in depth for LAN exposure). Enabled by
|
|
34
|
+
// setting `config.token` on this plugin's loader row (profile
|
|
35
|
+
// cordis.patch.yml override) or the DSH_MCP_MANAGER_TOKEN env var. When
|
|
36
|
+
// set, every state-changing op requires `x-dsh-token: <token>`. Read-only
|
|
37
|
+
// ops (plugin-version, mcpm-list, skill-list) stay open so the UI still
|
|
38
|
+
// renders; mcpm-export is guarded too because it leaks full configs.
|
|
39
|
+
const TOKEN = String(ctx.config?.token || process.env.DSH_MCP_MANAGER_TOKEN || '').trim();
|
|
40
|
+
const WRITE_OPS = new Set([
|
|
41
|
+
'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-restart',
|
|
42
|
+
'mcpm-export', 'mcpm-import', 'skill-toggle',
|
|
43
|
+
]);
|
|
25
44
|
const wait = (ms) => ctx.timeout(ms);
|
|
26
45
|
const message = (e) => String((e && e.message) || e);
|
|
27
46
|
let writeChain = Promise.resolve();
|
|
@@ -137,6 +156,10 @@ export default {
|
|
|
137
156
|
});
|
|
138
157
|
}
|
|
139
158
|
// ---------- path discovery ----------
|
|
159
|
+
// Known limitation: profile detection probes 'web' then 'headless' by
|
|
160
|
+
// presence of profiles/<name>/cordis.patch.yml, then falls back to any
|
|
161
|
+
// profile that has one, and finally to 'web'. A profile whose directory
|
|
162
|
+
// name matches none of these and has no patch file yet is not detected.
|
|
140
163
|
let cached = null;
|
|
141
164
|
async function ensurePaths() {
|
|
142
165
|
if (cached)
|
|
@@ -266,6 +289,11 @@ export default {
|
|
|
266
289
|
].join('\n');
|
|
267
290
|
}
|
|
268
291
|
// ---------- YAML parsing (mini parser) ----------
|
|
292
|
+
// Known limitation: this hand-rolled parser assumes the exact indentation
|
|
293
|
+
// style that buildInsertBlock emits (config at 6 spaces, children at 8,
|
|
294
|
+
// nested maps/lists at 10+). Hand-edited patch files using different
|
|
295
|
+
// indentation may parse incorrectly — DSH itself only cares about the
|
|
296
|
+
// effective YAML it reads, and this parser exists purely for the UI.
|
|
269
297
|
function splitKV(text) {
|
|
270
298
|
const m = text.match(/^("(?:\\.|[^"])*"|'[^']*'|[^:]+?)\s*:\s*(.*)$/);
|
|
271
299
|
if (!m)
|
|
@@ -626,6 +654,9 @@ export default {
|
|
|
626
654
|
return { ok: true, row };
|
|
627
655
|
}
|
|
628
656
|
// ---------- ops ----------
|
|
657
|
+
async function pluginVersion() {
|
|
658
|
+
return { ok: true, version: PKG_VERSION };
|
|
659
|
+
}
|
|
629
660
|
async function mcpmList() {
|
|
630
661
|
const p = await ensurePaths();
|
|
631
662
|
const rows = [];
|
|
@@ -767,14 +798,32 @@ export default {
|
|
|
767
798
|
const block = buildInsertBlock(row);
|
|
768
799
|
return withWriteLock(async () => {
|
|
769
800
|
if (oldAbs !== newAbs) {
|
|
770
|
-
|
|
801
|
+
// Level migration: remove from the old file, insert into the new one.
|
|
802
|
+
// Not atomic, so keep the old content and restore it if the second
|
|
803
|
+
// write fails — losing the entry is worse than a transient dup.
|
|
804
|
+
const origOld = await readPatch(oldAbs);
|
|
805
|
+
let c = origOld;
|
|
771
806
|
c = removeEntryAll(c, id);
|
|
772
|
-
|
|
807
|
+
try {
|
|
808
|
+
await writePatch(oldAbs, c);
|
|
809
|
+
}
|
|
810
|
+
catch (e) {
|
|
811
|
+
return { ok: false, error: '写入失败: ' + message(e) };
|
|
812
|
+
}
|
|
773
813
|
let c2 = await readPatch(newAbs);
|
|
774
814
|
c2 = appendBlock(c2, block);
|
|
775
815
|
if (cur.disabled)
|
|
776
816
|
c2 = appendBlock(c2, buildDisableBlock(id, true));
|
|
777
|
-
|
|
817
|
+
try {
|
|
818
|
+
await writePatch(newAbs, c2);
|
|
819
|
+
}
|
|
820
|
+
catch (e) {
|
|
821
|
+
try {
|
|
822
|
+
await writePatch(oldAbs, origOld);
|
|
823
|
+
}
|
|
824
|
+
catch (e2) { /* best effort */ }
|
|
825
|
+
return { ok: false, error: '写入失败(已回滚): ' + message(e) };
|
|
826
|
+
}
|
|
778
827
|
}
|
|
779
828
|
else {
|
|
780
829
|
let c = await readPatch(newAbs);
|
|
@@ -799,6 +848,11 @@ export default {
|
|
|
799
848
|
return withWriteLock(async () => {
|
|
800
849
|
let c = await readPatch(abs);
|
|
801
850
|
if (enabled) {
|
|
851
|
+
// Drop every `disabled: true` override for this id. If the insert row
|
|
852
|
+
// itself still says disabled (e.g. user hand-edited it), append an
|
|
853
|
+
// explicit `disabled: false` override so the effective state flips.
|
|
854
|
+
// Enable overrides are intentionally left in place — they are the
|
|
855
|
+
// mechanism that lets a disabled-by-default row be turned on.
|
|
802
856
|
c = removeMarked(c, id, 'disable');
|
|
803
857
|
const { rows } = parseRows(c);
|
|
804
858
|
const row = rows.find((r) => r.id === id);
|
|
@@ -826,25 +880,30 @@ export default {
|
|
|
826
880
|
c = removeMarked(c, id, 'enable');
|
|
827
881
|
c = appendBlock(c, buildDisableBlock(id, true));
|
|
828
882
|
await writePatch(abs, c);
|
|
883
|
+
const warnings = [];
|
|
829
884
|
if (pluginInventory) {
|
|
830
|
-
await waitFor(async () => {
|
|
885
|
+
const off = await waitFor(async () => {
|
|
831
886
|
const e = await liveEntry(id);
|
|
832
887
|
return e ? e.enabled === false : false;
|
|
833
888
|
}, 5000, 300);
|
|
889
|
+
if (!off)
|
|
890
|
+
warnings.push('loader 未在 5 秒内停用该服务');
|
|
834
891
|
}
|
|
835
892
|
await wait(1000);
|
|
836
893
|
c = await readPatch(abs);
|
|
837
894
|
c = removeMarked(c, id, 'disable');
|
|
838
895
|
await writePatch(abs, c);
|
|
839
896
|
if (pluginInventory) {
|
|
840
|
-
await waitFor(async () => {
|
|
897
|
+
const on = await waitFor(async () => {
|
|
841
898
|
const e = await liveEntry(id);
|
|
842
899
|
return e ? e.enabled === true : false;
|
|
843
900
|
}, 5000, 300);
|
|
901
|
+
if (!on)
|
|
902
|
+
warnings.push('loader 未在 5 秒内重新启用该服务');
|
|
844
903
|
}
|
|
845
904
|
else
|
|
846
905
|
await wait(1500);
|
|
847
|
-
return { ok: true };
|
|
906
|
+
return warnings.length ? { ok: true, warning: warnings.join(';') } : { ok: true };
|
|
848
907
|
});
|
|
849
908
|
}
|
|
850
909
|
async function mcpmRemove(args) {
|
|
@@ -906,28 +965,27 @@ export default {
|
|
|
906
965
|
continue;
|
|
907
966
|
}
|
|
908
967
|
const row = norm.row;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
if (existing.serverNames.has(row.serverName)) {
|
|
915
|
-
skipped.push({ id: row.id, reason: 'serverName 已存在' });
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
const abs = row.level === 'global' ? p.globalPatch : p.projectPatch;
|
|
968
|
+
// Existence checks run INSIDE the write lock so two concurrent imports
|
|
969
|
+
// (or an import racing an add) cannot both pass the same-id/same-name
|
|
970
|
+
// check and duplicate rows (TOCTOU).
|
|
919
971
|
const res = await withWriteLock(async () => {
|
|
972
|
+
const existing = await collectAll();
|
|
973
|
+
if (existing.ids.has(row.id))
|
|
974
|
+
return { skipped: true, reason: 'id 已存在' };
|
|
975
|
+
if (existing.serverNames.has(row.serverName))
|
|
976
|
+
return { skipped: true, reason: 'serverName 已存在' };
|
|
977
|
+
const abs = row.level === 'global' ? p.globalPatch : p.projectPatch;
|
|
920
978
|
let c = await readPatch(abs);
|
|
921
979
|
c = appendBlock(c, buildInsertBlock(row));
|
|
922
980
|
if (row.disabled)
|
|
923
981
|
c = appendBlock(c, buildDisableBlock(row.id, true));
|
|
924
982
|
await writePatch(abs, c);
|
|
925
|
-
return {
|
|
983
|
+
return { added: true };
|
|
926
984
|
});
|
|
927
|
-
if (res
|
|
985
|
+
if (res.added)
|
|
928
986
|
added.push(row.id);
|
|
929
987
|
else
|
|
930
|
-
skipped.push({ id: row.id, reason: (res && res.
|
|
988
|
+
skipped.push({ id: row.id, reason: (res && res.reason) || '写入失败' });
|
|
931
989
|
}
|
|
932
990
|
return { ok: true, added, skipped };
|
|
933
991
|
}
|
|
@@ -948,6 +1006,7 @@ export default {
|
|
|
948
1006
|
return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter((s) => s !== '');
|
|
949
1007
|
}
|
|
950
1008
|
const handlers = {
|
|
1009
|
+
'plugin-version': pluginVersion,
|
|
951
1010
|
'mcpm-list': mcpmList,
|
|
952
1011
|
'mcpm-add': mcpmAdd,
|
|
953
1012
|
'mcpm-edit': mcpmEdit,
|
|
@@ -1030,9 +1089,21 @@ export default {
|
|
|
1030
1089
|
}));
|
|
1031
1090
|
// ---------- HTTP API route (UI half), registered defensively ----------
|
|
1032
1091
|
if (webServer) {
|
|
1092
|
+
// Cap request bodies (1 MiB) — the API has no legitimate large payloads,
|
|
1093
|
+
// and unbounded buffering would let a local attacker exhaust memory.
|
|
1094
|
+
const MAX_BODY = 1024 * 1024;
|
|
1033
1095
|
const readBody = (req) => new Promise((resolve, reject) => {
|
|
1034
1096
|
const chunks = [];
|
|
1035
|
-
|
|
1097
|
+
let size = 0;
|
|
1098
|
+
req.on('data', (c) => {
|
|
1099
|
+
const s = String(c);
|
|
1100
|
+
size += s.length;
|
|
1101
|
+
if (size > MAX_BODY) {
|
|
1102
|
+
reject(new Error('request body too large'));
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
chunks.push(s);
|
|
1106
|
+
});
|
|
1036
1107
|
req.on('end', () => resolve(chunks.join('')));
|
|
1037
1108
|
req.on('error', reject);
|
|
1038
1109
|
});
|
|
@@ -1086,8 +1157,18 @@ export default {
|
|
|
1086
1157
|
try {
|
|
1087
1158
|
payload = JSON.parse((await readBody(req)) || '{}');
|
|
1088
1159
|
}
|
|
1089
|
-
catch (e) {
|
|
1160
|
+
catch (e) {
|
|
1161
|
+
if (String(e?.message).includes('body too large')) {
|
|
1162
|
+
res.end(JSON.stringify({ ok: false, error: '请求体过大' }));
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
/* otherwise fall through with {} */
|
|
1166
|
+
}
|
|
1090
1167
|
const op = String(payload.op || '');
|
|
1168
|
+
if (TOKEN && WRITE_OPS.has(op) && hdr('x-dsh-token') !== TOKEN) {
|
|
1169
|
+
res.end(JSON.stringify({ ok: false, error: '缺少或错误的访问令牌(x-dsh-token)' }));
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1091
1172
|
const fn = handlers[op];
|
|
1092
1173
|
if (!fn) {
|
|
1093
1174
|
res.end(JSON.stringify({ ok: false, error: '未知操作: ' + op }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xxxyz/dsh-mcp-manager",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "DSH-standard MCP manager plugin: Settings UI + HTTP API + model-facing mcp_manager_* tools. Install with one command: dsh plugin --profile web add @xxxyz/dsh-mcp-manager@latest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc -p tsconfig.json",
|
|
29
29
|
"test": "npm run build && node --test",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
30
|
+
"prepublishOnly": "npm run build",
|
|
31
|
+
"lint": "node --check lib/client.js && node --check lib/index.js"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
|
33
34
|
"dsh",
|