@moon16u/dsh-plugin-mcp-console 0.2.1 → 0.3.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/client.js +95 -12
- package/lib/index.js +35 -51
- package/lib/routes.js +21 -3
- package/lib/store.js +10 -0
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Then open DSH Web → bottom-left Settings → **"MCP servers"**.
|
|
|
36
36
|
| Auto-ingest | MCP entries written into `cordis.patch.yml` by external agents/scripts are migrated into dynamic management on boot and on "Sync config & refresh status" (store persists before the YAML is trimmed — configuration is never lost) |
|
|
37
37
|
| YAML export | `POST /api/dsh-mcp-console/export-yaml` restores dynamic config as standard `cordis.patch.yml` entries (uninstall/backup reversibility) |
|
|
38
38
|
| Model awareness | A `ctx.systemPrompt.section` announcement tells every agent that MCP management lives in the settings page, not in hand-edited config files (own inject scope; dormant on hosts without the service) |
|
|
39
|
-
| Settings integration | `enabled` / `announceToAgent` master switches
|
|
39
|
+
| Settings integration | `enabled` / `announceToAgent` master switches as `.volatile()` fields of the schema-discovered `export const Config` (rc.2 SettingsForms): editable from the settings GUI and hot-updated in place, no DSH restart — `enabled` tears the whole surface down (routes + MCP connections + announcement; stores preserved), `announceToAgent` toggles only the announcement; hosts without the service fall back to entry defaults |
|
|
40
40
|
| Settings-GUI card | The "MCP console" card on Settings → Plugins (`settings.plugin.item`, key `mcp-console`): collapsible and shaped like the official cards (same `--dsw-alias-*` tokens and metrics), switches write immediately, and a changed field carries a "Customized" badge with its own reset |
|
|
41
41
|
| Degraded state | With `enabled` off, the "MCP servers" page stops reporting an API error and shows the switched-off notice plus the way back (an authenticated 404 on our own prefix is the tell), and keeps the SSE stream closed instead of retrying |
|
|
42
42
|
|
package/README.zh-CN.md
CHANGED
|
@@ -29,7 +29,7 @@ dsh plugin --profile web add @moon16u/dsh-plugin-mcp-console
|
|
|
29
29
|
| Auto-Ingest | 外部 agent / 脚本按官方推荐写入 `cordis.patch.yml` 的 MCP 条目,在启动与「同步配置并刷新状态」时自动移入动态管理(store 持久化成功才修剪 YAML,绝不丢配置) |
|
|
30
30
|
| YAML 回写 | `POST /api/dsh-mcp-console/export-yaml` 把动态配置一键还原为标准 `cordis.patch.yml` 条目(卸载/备份可逆) |
|
|
31
31
|
| 模型感知 | 通过 `ctx.systemPrompt.section` 向每个 agent 的系统提示注入一段公告:MCP 管理请到设置页操作,勿手改配置文件(独立 inject 作用域,无该服务的宿主上自动休眠) |
|
|
32
|
-
| 设置集成 | `enabled` / `announceToAgent`
|
|
32
|
+
| 设置集成 | `enabled` / `announceToAgent` 总开关为 schema 自发现的 `export const Config` 中的 `.volatile()` 字段(rc.2 SettingsForms):设置 GUI 可改、就地热更新,无需重启 DSH:`enabled` 关闭整体下线(路由 + MCP 连接 + 公告,store 保留),`announceToAgent` 只控制公告;无 settings 服务的宿主回落条目默认值 |
|
|
33
33
|
| 设置 GUI 卡片 | 设置 → 插件 页的「MCP 控制台」卡片(`settings.plugin.item`,键 `mcp-console`):可折叠、与官方卡片同形(同一批 `--dsw-alias-*` 令牌与尺寸),开关即改即写,被改过的字段带「已自定义」徽章与逐字段「恢复默认」 |
|
|
34
34
|
| 停用后的降级 | `enabled` 关闭后「MCP 服务器」页不再报接口错误,改为显示停用提示与恢复入口(自有前缀路由的 404 即判定为已停用),并停开 SSE 以免反复重试 |
|
|
35
35
|
|
package/lib/client.js
CHANGED
|
@@ -9,6 +9,13 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
// ==== mcp-console client body (synced; keep identical to dsh-pouch/lib/client.js) ====
|
|
10
10
|
|
|
11
11
|
var McpIcons = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
12
|
+
var IconRefresh = McpIcons.IconRefreshOutlineRegular || McpIcons.IconRefreshOutline16;
|
|
13
|
+
var IconPlus = McpIcons.IconPlusOutlineRegular || McpIcons.IconPlusOutline16;
|
|
14
|
+
var IconClose = McpIcons.IconCloseOutlineRegular || McpIcons.IconCloseOutline16;
|
|
15
|
+
var IconTrash = McpIcons.IconTrashOutlineRegular || McpIcons.IconTrashOutline16;
|
|
16
|
+
var IconLink = McpIcons.IconLinkOutlineRegular || McpIcons.IconLinkOutline16;
|
|
17
|
+
var IconChevronRight = McpIcons.IconChevronRightOutlineRegular || McpIcons.IconChevronRightOutline14;
|
|
18
|
+
var IconChevronDown = McpIcons.IconChevronDownOutlineRegular || McpIcons.IconChevronDownOutline14;
|
|
12
19
|
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
// mcp-console client: an "MCP" settings section (mcp-manager-gui-spec.md)
|
|
@@ -243,6 +250,7 @@ window.__ModuleLoader__.load({
|
|
|
243
250
|
".mcp-plugin-card{list-style:none;margin:0;border:.5px solid var(--dsw-alias-border-l4,#e5e7eb);border-radius:16px;background:var(--dsw-alias-bg-layer-3,#ffffff);transition:border-color .16s,background .16s}",
|
|
244
251
|
".mcp-plugin-card:hover{border-color:var(--dsw-alias-label-dimmed,#d1d5db)}",
|
|
245
252
|
".mcp-plugin-card[data-open=true]{background:var(--dsw-alias-bg-layer-2,#fafafa);border-color:var(--dsw-alias-label-dimmed,#d1d5db)}",
|
|
253
|
+
".mcp-plugin-card-flat .mcp-plugin-body{border-top:none;padding-top:0;padding-bottom:0}",
|
|
246
254
|
".mcp-plugin-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}",
|
|
247
255
|
".mcp-plugin-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}",
|
|
248
256
|
".mcp-plugin-headtext{display:flex;flex-direction:column;flex:1;gap:4px;min-width:0}",
|
|
@@ -486,12 +494,12 @@ window.__ModuleLoader__.load({
|
|
|
486
494
|
disabled: busy || (props.probe && props.probe.busy === true),
|
|
487
495
|
title: t("test"), "aria-label": t("test"),
|
|
488
496
|
onClick: function () { props.onProbe(server); },
|
|
489
|
-
}, McpH(
|
|
497
|
+
}, McpH(IconLink, { size: 14 })),
|
|
490
498
|
readOnly ? null : McpH("button", {
|
|
491
499
|
type: "button", className: "mcp-mgr-del", disabled: busy,
|
|
492
500
|
title: t("remove"), "aria-label": t("remove"),
|
|
493
501
|
onClick: function () { props.onRemove(server); },
|
|
494
|
-
}, McpH(
|
|
502
|
+
}, McpH(IconTrash, { size: 14 })),
|
|
495
503
|
McpH(McpSwitch, {
|
|
496
504
|
t: t, enabled: enabled, disabled: busy || readOnly,
|
|
497
505
|
onChange: function (next) { if (!readOnly) props.onToggleServer(server, next); },
|
|
@@ -689,7 +697,7 @@ window.__ModuleLoader__.load({
|
|
|
689
697
|
"aria-expanded": advancedOpen ? "true" : "false",
|
|
690
698
|
onClick: function () { setAdvancedOpen(!advancedOpen); },
|
|
691
699
|
},
|
|
692
|
-
McpH(
|
|
700
|
+
McpH(IconChevronRight, { size: 14, style: { transform: advancedOpen ? "rotate(90deg)" : "none", transition: "transform .15s ease" } }),
|
|
693
701
|
t("formAdvanced")),
|
|
694
702
|
advancedOpen
|
|
695
703
|
? McpH("div", { className: "mcp-adv-box" },
|
|
@@ -784,12 +792,78 @@ window.__ModuleLoader__.load({
|
|
|
784
792
|
}, t("importConfirm"))));
|
|
785
793
|
}
|
|
786
794
|
|
|
795
|
+
function createMcpScope() {
|
|
796
|
+
var listeners = new Set();
|
|
797
|
+
var config = { enabled: true, announceToAgent: true };
|
|
798
|
+
var user = {};
|
|
799
|
+
var status = "ready";
|
|
800
|
+
var fetched = false;
|
|
801
|
+
|
|
802
|
+
function notify() {
|
|
803
|
+
listeners.forEach(function (l) { try { l(); } catch (e) {} });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function fetchConfig() {
|
|
807
|
+
if (fetched) return;
|
|
808
|
+
fetched = true;
|
|
809
|
+
mcpApi("/health").then(function (health) {
|
|
810
|
+
if (health && health.config) {
|
|
811
|
+
config = {
|
|
812
|
+
enabled: health.config.enabled !== false,
|
|
813
|
+
announceToAgent: health.config.announceToAgent !== false,
|
|
814
|
+
};
|
|
815
|
+
status = "ready";
|
|
816
|
+
notify();
|
|
817
|
+
}
|
|
818
|
+
}, function () {
|
|
819
|
+
config = { enabled: false, announceToAgent: true };
|
|
820
|
+
status = "ready";
|
|
821
|
+
notify();
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
subscribe: function (listener) {
|
|
827
|
+
listeners.add(listener);
|
|
828
|
+
return function () { listeners.delete(listener); };
|
|
829
|
+
},
|
|
830
|
+
getSnapshot: function () {
|
|
831
|
+
fetchConfig();
|
|
832
|
+
return {
|
|
833
|
+
status: status,
|
|
834
|
+
value: config,
|
|
835
|
+
user: user,
|
|
836
|
+
writable: true,
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
set: function (field, value) {
|
|
840
|
+
user[field] = value;
|
|
841
|
+
config[field] = value;
|
|
842
|
+
notify();
|
|
843
|
+
return mcpApi("/config", {
|
|
844
|
+
method: "POST",
|
|
845
|
+
body: JSON.stringify({ config: { [field]: value } }),
|
|
846
|
+
}).catch(function () {});
|
|
847
|
+
},
|
|
848
|
+
unset: function (field) {
|
|
849
|
+
delete user[field];
|
|
850
|
+
config[field] = true;
|
|
851
|
+
notify();
|
|
852
|
+
return mcpApi("/config", {
|
|
853
|
+
method: "POST",
|
|
854
|
+
body: JSON.stringify({ config: { [field]: true } }),
|
|
855
|
+
}).catch(function () {});
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
787
860
|
/**
|
|
788
861
|
* The settings section (mcp-manager-gui-spec.md §1–§3): header bar with
|
|
789
862
|
* refresh + add + import, the server list card, and modals.
|
|
790
863
|
*/
|
|
791
864
|
function McpSection(props) {
|
|
792
865
|
var t = props.t;
|
|
866
|
+
var scope = props.scope || createMcpScope();
|
|
793
867
|
var useState = React.useState;
|
|
794
868
|
var useEffect = React.useEffect;
|
|
795
869
|
|
|
@@ -1021,7 +1095,7 @@ window.__ModuleLoader__.load({
|
|
|
1021
1095
|
McpH("button", {
|
|
1022
1096
|
type: "button", className: "mcp-mgr-iconbtn", "aria-label": t("cancel"),
|
|
1023
1097
|
onClick: function () { setMode(null); setEditing(null); },
|
|
1024
|
-
}, McpH(
|
|
1098
|
+
}, McpH(IconClose, { size: 14 }))),
|
|
1025
1099
|
McpH(McpServerForm, {
|
|
1026
1100
|
t: t,
|
|
1027
1101
|
initial: editing,
|
|
@@ -1037,7 +1111,7 @@ window.__ModuleLoader__.load({
|
|
|
1037
1111
|
McpH("button", {
|
|
1038
1112
|
type: "button", className: "mcp-mgr-iconbtn", "aria-label": t("cancel"),
|
|
1039
1113
|
onClick: function () { setMode(null); },
|
|
1040
|
-
}, McpH(
|
|
1114
|
+
}, McpH(IconClose, { size: 14 }))),
|
|
1041
1115
|
McpH(McpImportDialog, {
|
|
1042
1116
|
t: t,
|
|
1043
1117
|
onCancel: function () { setMode(null); },
|
|
@@ -1058,7 +1132,7 @@ window.__ModuleLoader__.load({
|
|
|
1058
1132
|
McpH("button", {
|
|
1059
1133
|
type: "button", className: "mcp-mgr-iconbtn", "aria-label": t("cancel"),
|
|
1060
1134
|
onClick: function () { setConfirm(null); },
|
|
1061
|
-
}, McpH(
|
|
1135
|
+
}, McpH(IconClose, { size: 14 }))),
|
|
1062
1136
|
McpH("div", { className: "mcp-form-body" },
|
|
1063
1137
|
McpH("p", { className: "mcp-mgr-note" }, t("removeConfirm"))),
|
|
1064
1138
|
McpH("div", { className: "mcp-form-footer" },
|
|
@@ -1080,6 +1154,7 @@ window.__ModuleLoader__.load({
|
|
|
1080
1154
|
return McpH("div", { className: "mcp-mgr", "data-mcp-console-section": "", "data-mcp-console-offline": "" },
|
|
1081
1155
|
McpH("div", { className: "mcp-mgr-head" },
|
|
1082
1156
|
McpH("h2", { className: "mcp-mgr-title" }, t("title"))),
|
|
1157
|
+
McpH(McpPluginCard, { t: t, scope: scope, hideHeader: true }),
|
|
1083
1158
|
McpH("p", { className: "mcp-mgr-note" }, t("sectionOffline")),
|
|
1084
1159
|
McpH("p", { className: "mcp-ext" }, t("sectionOfflineHint")));
|
|
1085
1160
|
}
|
|
@@ -1091,15 +1166,16 @@ window.__ModuleLoader__.load({
|
|
|
1091
1166
|
type: "button", className: "mcp-mgr-iconbtn", disabled: refreshing,
|
|
1092
1167
|
title: t("refresh"), "aria-label": t("refresh"),
|
|
1093
1168
|
onClick: refresh,
|
|
1094
|
-
}, McpH(
|
|
1169
|
+
}, McpH(IconRefresh, { size: 15 })),
|
|
1095
1170
|
McpH("button", {
|
|
1096
1171
|
type: "button", className: "mcp-mgr-btn", disabled: topDisabled,
|
|
1097
1172
|
onClick: function () { setEditing(null); setMode("add"); },
|
|
1098
|
-
}, McpH(
|
|
1173
|
+
}, McpH(IconPlus, { size: 14 }), t("add")),
|
|
1099
1174
|
McpH("button", {
|
|
1100
1175
|
type: "button", className: "mcp-mgr-btn", disabled: topDisabled,
|
|
1101
1176
|
onClick: function () { setMode("import"); },
|
|
1102
1177
|
}, t("importAction"))),
|
|
1178
|
+
McpH(McpPluginCard, { t: t, scope: scope, hideHeader: true }),
|
|
1103
1179
|
snapshot
|
|
1104
1180
|
? McpH("div", { className: "mcp-mgr-list" }, listChildren)
|
|
1105
1181
|
: McpH("p", { className: "mcp-mgr-note" }, t("loadingData")),
|
|
@@ -1227,10 +1303,16 @@ window.__ModuleLoader__.load({
|
|
|
1227
1303
|
*/
|
|
1228
1304
|
function McpPluginCard(props) {
|
|
1229
1305
|
var t = props.t;
|
|
1230
|
-
|
|
1306
|
+
if (props.hideHeader === true) {
|
|
1307
|
+
return McpH("div", {
|
|
1308
|
+
className: "mcp-plugin-card mcp-plugin-card-flat",
|
|
1309
|
+
"data-mcp-plugin-card": "",
|
|
1310
|
+
}, McpH(McpPluginCardFields, { t: t, scope: props.scope }));
|
|
1311
|
+
}
|
|
1312
|
+
var openState = React.useState(props.defaultOpen === true);
|
|
1231
1313
|
var open = openState[0], setOpen = openState[1];
|
|
1232
1314
|
var title = t("pluginCardTitle");
|
|
1233
|
-
return McpH("
|
|
1315
|
+
return McpH("div", {
|
|
1234
1316
|
className: "mcp-plugin-card",
|
|
1235
1317
|
"data-open": open ? "true" : "false",
|
|
1236
1318
|
"data-mcp-plugin-card": "",
|
|
@@ -1245,7 +1327,7 @@ window.__ModuleLoader__.load({
|
|
|
1245
1327
|
McpH("span", { className: "mcp-plugin-headtext" },
|
|
1246
1328
|
McpH("span", { className: "mcp-plugin-name" }, title),
|
|
1247
1329
|
McpH("span", { className: "mcp-plugin-desc" }, t("pluginCardDesc"))),
|
|
1248
|
-
McpH(
|
|
1330
|
+
McpH(IconChevronDown, { size: 14, className: "mcp-plugin-chevron" })),
|
|
1249
1331
|
open ? McpH(McpPluginCardFields, { t: t, scope: props.scope }) : null);
|
|
1250
1332
|
}
|
|
1251
1333
|
|
|
@@ -1255,6 +1337,7 @@ window.__ModuleLoader__.load({
|
|
|
1255
1337
|
}, "mcp-console: dictionaries");
|
|
1256
1338
|
|
|
1257
1339
|
var mcpT = ctx.locale.bind(McpNS);
|
|
1340
|
+
var mcpDefaultScope = createMcpScope();
|
|
1258
1341
|
|
|
1259
1342
|
ctx.slots.inject("settings.section", function () {
|
|
1260
1343
|
return ctx.slots.register({
|
|
@@ -1262,7 +1345,7 @@ window.__ModuleLoader__.load({
|
|
|
1262
1345
|
id: "mcp-console",
|
|
1263
1346
|
order: 12,
|
|
1264
1347
|
label: function () { return mcpT("nav"); },
|
|
1265
|
-
inject: function () { return { t: mcpT }; },
|
|
1348
|
+
inject: function () { return { t: mcpT, scope: mcpDefaultScope }; },
|
|
1266
1349
|
}, McpSection);
|
|
1267
1350
|
});
|
|
1268
1351
|
|
package/lib/index.js
CHANGED
|
@@ -33,52 +33,32 @@ export const name = "mcp-console";
|
|
|
33
33
|
/** Settings namespace the master switches live under (provider contract: [a-z0-9-]). */
|
|
34
34
|
export const MCP_CONSOLE_SETTINGS_NAMESPACE = "mcp-console";
|
|
35
35
|
|
|
36
|
-
/** Plugin config: master switches, editable from the settings GUI.
|
|
36
|
+
/** Plugin config: master switches, editable from the settings GUI.
|
|
37
|
+
*
|
|
38
|
+
* rc.2 (dsh-settings schema-driven): `SettingsForms` discovers this schema from
|
|
39
|
+
* the plugin's `export const Config`, and only `.volatile()` fields are editable
|
|
40
|
+
* in the GUI — a volatile field is hot-updated in place on the config reference
|
|
41
|
+
* the fiber already holds, without a fiber restart. */
|
|
37
42
|
export const Config = z.object({
|
|
38
43
|
/** Master switch: routes + MCP fibers + SSE (stores are never touched). */
|
|
39
|
-
enabled: z.boolean().default(true),
|
|
44
|
+
enabled: z.boolean().default(true).volatile(),
|
|
40
45
|
/** Register the model-facing system-prompt announcement. */
|
|
41
|
-
announceToAgent: z.boolean().default(true),
|
|
46
|
+
announceToAgent: z.boolean().default(true).volatile(),
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
announceToAgent: value?.announceToAgent !== false,
|
|
49
|
-
};
|
|
49
|
+
/** Unwrap a cosmokit volatile reference (rc.2 volatile config) to its current
|
|
50
|
+
* value; plain values (tests, non-volatile hosts) pass through unchanged. */
|
|
51
|
+
function unwrapValue(v) {
|
|
52
|
+
return v !== null && typeof v === "object" && typeof v.get === "function" ? v.get() : v;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
60
|
-
const settings = settingsCtx.settings;
|
|
61
|
-
if (typeof settings?.installSection === "function") {
|
|
62
|
-
settings.installSection(ctx, ns, schema, entry, hooks);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (typeof settings?.register !== "function") return;
|
|
66
|
-
const scope = settings.register(ns, schema, {
|
|
67
|
-
base: entry,
|
|
68
|
-
...(hooks.validate === void 0 ? {} : { validate: hooks.validate }),
|
|
69
|
-
});
|
|
70
|
-
hooks.setSource(() => scope.get());
|
|
71
|
-
settingsCtx.effect?.(() => () => {
|
|
72
|
-
if (ctx.fiber?.state === 4 || ctx.fiber?.state === 5) return;
|
|
73
|
-
hooks.setSource(() => entry);
|
|
74
|
-
hooks.onChange();
|
|
75
|
-
});
|
|
76
|
-
hooks.onChange();
|
|
77
|
-
scope.watch(() => {
|
|
78
|
-
if (ctx.fiber?.state === 4 || ctx.fiber?.state === 5) return;
|
|
79
|
-
hooks.onChange();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
55
|
+
/** Resolve a raw config value (plain or volatile refs) onto the switch defaults. */
|
|
56
|
+
export function resolveConfig(value, storedConfig) {
|
|
57
|
+
const merged = { ...storedConfig, ...(value || {}) };
|
|
58
|
+
return {
|
|
59
|
+
enabled: unwrapValue(merged?.enabled) !== false,
|
|
60
|
+
announceToAgent: unwrapValue(merged?.announceToAgent) !== false,
|
|
61
|
+
};
|
|
82
62
|
}
|
|
83
63
|
|
|
84
64
|
/**
|
|
@@ -98,8 +78,10 @@ const SYSTEM_PROMPT_GUIDANCE =
|
|
|
98
78
|
const ANNOUNCEMENT_ORDER = 160;
|
|
99
79
|
|
|
100
80
|
export function apply(ctx, config) {
|
|
101
|
-
|
|
102
|
-
|
|
81
|
+
// rc.2: `config` may carry cosmokit volatile refs whose values hot-update in
|
|
82
|
+
// place; keep `current` pointing at it so resolveConfig() reads live values.
|
|
83
|
+
let current = () => config;
|
|
84
|
+
const globalStore = new Store(globalStorePath());
|
|
103
85
|
|
|
104
86
|
/** Live surfaces, each an inject fiber that can be torn down and rebuilt. */
|
|
105
87
|
let compositionFiber = null;
|
|
@@ -131,7 +113,8 @@ export function apply(ctx, config) {
|
|
|
131
113
|
* have rebuilt them meanwhile), so disposals run from captured locals last.
|
|
132
114
|
*/
|
|
133
115
|
async function syncStep() {
|
|
134
|
-
const
|
|
116
|
+
const storedConfig = globalStore?.read?.()?.config;
|
|
117
|
+
const value = resolveConfig(current(), storedConfig);
|
|
135
118
|
const wantAnnouncement = value.enabled && value.announceToAgent;
|
|
136
119
|
const toDispose = [];
|
|
137
120
|
if (wantAnnouncement && !announcementFiber) {
|
|
@@ -218,7 +201,8 @@ export function apply(ctx, config) {
|
|
|
218
201
|
const routes = registerRoutes(scope, {
|
|
219
202
|
orchestrator,
|
|
220
203
|
uiStore: globalStore,
|
|
221
|
-
getPluginConfig: () => resolveConfig(current()),
|
|
204
|
+
getPluginConfig: () => resolveConfig(current(), globalStore.read().config),
|
|
205
|
+
onConfigChange: () => sync(),
|
|
222
206
|
});
|
|
223
207
|
const notify = () => routes.notify();
|
|
224
208
|
|
|
@@ -256,14 +240,14 @@ export function apply(ctx, config) {
|
|
|
256
240
|
}, "mcp-console: composition");
|
|
257
241
|
}
|
|
258
242
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
243
|
+
// rc.2 settings: SettingsForms discovers this plugin's schema from
|
|
244
|
+
// `export const Config` (above); the volatile switches are hot-updated in
|
|
245
|
+
// place on the `config` reference this fiber holds, so no registration call
|
|
246
|
+
// is needed and `current` keeps reading the live value. A GUI edit emits
|
|
247
|
+
// `settings/document-updated`; re-sync on it — sync() is idempotent and
|
|
248
|
+
// resolveConfig() reads the latest volatile value.
|
|
249
|
+
// A real cordis context has `on`; the fake ctx in unit tests omits it.
|
|
250
|
+
if (typeof ctx.on === "function") ctx.on("settings/document-updated", () => sync());
|
|
267
251
|
|
|
268
252
|
sync();
|
|
269
253
|
}
|
package/lib/routes.js
CHANGED
|
@@ -54,7 +54,7 @@ const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
|
54
54
|
* resolved master switches (settings-GUI backed), surfaced in /health
|
|
55
55
|
* @returns {{ dispose: () => void, notify: () => void }} route handle
|
|
56
56
|
*/
|
|
57
|
-
export function registerRoutes(ctx, { orchestrator, uiStore, getPluginConfig }) {
|
|
57
|
+
export function registerRoutes(ctx, { orchestrator, uiStore, getPluginConfig, onConfigChange }) {
|
|
58
58
|
const sseClients = new Set();
|
|
59
59
|
let heartbeat = null;
|
|
60
60
|
let notifyTimer = null;
|
|
@@ -177,12 +177,30 @@ export function registerRoutes(ctx, { orchestrator, uiStore, getPluginConfig })
|
|
|
177
177
|
|
|
178
178
|
if (segments[0] === "config") {
|
|
179
179
|
if (method === "GET") {
|
|
180
|
-
|
|
180
|
+
const storeDoc = uiStore.read();
|
|
181
|
+
respond(res, 200, {
|
|
182
|
+
ui: sanitizeUi(storeDoc.ui),
|
|
183
|
+
config: getPluginConfig ? getPluginConfig() : (storeDoc.config || null),
|
|
184
|
+
});
|
|
181
185
|
return;
|
|
182
186
|
}
|
|
183
187
|
if (method === "PUT" || method === "POST") {
|
|
184
188
|
const body = await readJson(req);
|
|
185
|
-
|
|
189
|
+
let updatedUi;
|
|
190
|
+
if (body?.ui !== undefined) {
|
|
191
|
+
updatedUi = sanitizeUi(uiStore.writeUi(sanitizeUi(body.ui)));
|
|
192
|
+
}
|
|
193
|
+
let updatedConfig;
|
|
194
|
+
if (body?.config !== undefined && typeof uiStore.writeConfig === "function") {
|
|
195
|
+
updatedConfig = uiStore.writeConfig(body.config);
|
|
196
|
+
if (typeof onConfigChange === "function") {
|
|
197
|
+
try { onConfigChange(updatedConfig); } catch {}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
respond(res, 200, {
|
|
201
|
+
ui: updatedUi ?? sanitizeUi(uiStore.read().ui),
|
|
202
|
+
config: updatedConfig ?? (getPluginConfig ? getPluginConfig() : null),
|
|
203
|
+
});
|
|
186
204
|
return;
|
|
187
205
|
}
|
|
188
206
|
respond(res, 405, { error: { code: "method_not_allowed", message: "GET or PUT /config" } });
|
package/lib/store.js
CHANGED
|
@@ -52,6 +52,7 @@ export function normalizeDoc(raw) {
|
|
|
52
52
|
version: typeof raw.version === "number" ? raw.version : STORE_VERSION,
|
|
53
53
|
servers: {},
|
|
54
54
|
...(isPlainObject(raw.ui) ? { ui: raw.ui } : {}),
|
|
55
|
+
...(isPlainObject(raw.config) ? { config: raw.config } : {}),
|
|
55
56
|
};
|
|
56
57
|
if (isPlainObject(raw.servers)) {
|
|
57
58
|
for (const [name, entry] of Object.entries(raw.servers)) {
|
|
@@ -123,6 +124,15 @@ export class Store {
|
|
|
123
124
|
return ui;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/** Persist only the plugin master config section, keeping servers untouched. */
|
|
128
|
+
writeConfig(config) {
|
|
129
|
+
if (!isPlainObject(config)) throw new Error("config must be an object");
|
|
130
|
+
const doc = this.read();
|
|
131
|
+
const updated = { ...(doc.config || {}), ...config };
|
|
132
|
+
this._persist({ ...doc, config: updated });
|
|
133
|
+
return updated;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
/** Atomically write the document (temp file + rename, no backups). */
|
|
127
137
|
_persist(doc) {
|
|
128
138
|
const body = JSON.stringify(doc, null, 2) + "\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moon16u/dsh-plugin-mcp-console",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Web GUI console for MCP servers: manages the official @deepseek-ai/dsh-mcp-client at runtime — add/edit/enable/disable/reconnect/delete servers, live status, mcpServers JSON import. No MCP protocol code: connection and tool registration are 100% the official client, resolved from the running DSH profile.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -34,13 +34,17 @@
|
|
|
34
34
|
"platform": "web"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.0.0",
|
|
39
|
+
"dsh": ">=0.2.0-rc.2"
|
|
40
|
+
},
|
|
37
41
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "
|
|
39
|
-
"@deepseek-ai/dsh-mcp-client": "
|
|
40
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
41
|
-
"@deepseek-ai/dsh-settings": "
|
|
42
|
-
"@deepseek-ai/dsh-tools": "
|
|
43
|
-
"@deepseek-ai/schemastery": "
|
|
42
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
43
|
+
"@deepseek-ai/dsh-mcp-client": ">=0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-settings": ">=0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
44
48
|
},
|
|
45
49
|
"peerDependenciesMeta": {
|
|
46
50
|
"@deepseek-ai/dsh-mcp-client": {
|
|
@@ -56,9 +60,6 @@
|
|
|
56
60
|
"optional": true
|
|
57
61
|
}
|
|
58
62
|
},
|
|
59
|
-
"engines": {
|
|
60
|
-
"node": ">=20.0.0"
|
|
61
|
-
},
|
|
62
63
|
"license": "MIT",
|
|
63
64
|
"publishConfig": {
|
|
64
65
|
"access": "public",
|