@moon16u/dsh-plugin-mcp-console 0.2.1 → 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/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 registered against the official settings provider (`mcp-console` namespace, dual-generation API): editable from the settings GUI with live effect — `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 |
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` 总开关走官方 settings provider(`mcp-console` 命名空间,双世代 API 兼容),设置 GUI 可改、即改即生效:`enabled` 关闭整体下线(路由 + MCP 连接 + 公告,store 保留),`announceToAgent` 只控制公告;无 settings 服务的宿主回落条目默认值 |
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(McpIcons.IconLinkOutline16, { size: 14 })),
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(McpIcons.IconTrashOutline16, { size: 14 })),
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(McpIcons.IconChevronRightOutline14, { size: 14, style: { transform: advancedOpen ? "rotate(90deg)" : "none", transition: "transform .15s ease" } }),
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(McpIcons.IconCloseOutline16, { size: 14 }))),
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(McpIcons.IconCloseOutline16, { size: 14 }))),
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(McpIcons.IconCloseOutline16, { size: 14 }))),
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(McpIcons.IconRefreshOutline16, { size: 15 })),
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(McpIcons.IconPlusOutline16, { size: 14 }), t("add")),
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
- var openState = React.useState(false);
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("li", {
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(McpIcons.IconChevronDownOutline14, { size: 14, className: "mcp-plugin-chevron" })),
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
- /** Resolve a raw config value (any shape) onto the switch defaults. */
45
- export function resolveConfig(value) {
46
- return {
47
- enabled: value?.enabled !== false,
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
- * Register this optional settings consumer on both the current provider API
54
- * (`settings.installSection`, dsh-settings >= 0.1.2) and the pre-0.1.2
55
- * `register` surface used by older hosts/tests. Same helper as the sibling
56
- * pouch plugins — kept verbatim so behavior cannot drift between packages.
57
- */
58
- function installSettingsSection(ctx, ns, schema, entry, hooks) {
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
- const entry = resolveConfig(config);
102
- let current = () => entry;
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 value = resolveConfig(current());
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
- // Registered AFTER the surface state exists: the pre-0.1.2 helper fires
260
- // onChange synchronously during registration, which must find sync() ready.
261
- installSettingsSection(ctx, MCP_CONSOLE_SETTINGS_NAMESPACE, Config, entry, {
262
- setSource: (source) => {
263
- current = source;
264
- },
265
- onChange: () => sync(),
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
- respond(res, 200, { ui: sanitizeUi(uiStore.read().ui) });
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
- respond(res, 200, { ui: sanitizeUi(uiStore.writeUi(sanitizeUi(body?.ui ?? {}))) });
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.2.1",
3
+ "version": "0.3.0",
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.1.7-rc.2"
40
+ },
37
41
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.2",
39
- "@deepseek-ai/dsh-mcp-client": "^0.1.5-rc.1",
40
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
41
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
42
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
43
- "@deepseek-ai/schemastery": "^3.18.2"
42
+ "@deepseek-ai/cordis": "~4.0.4",
43
+ "@deepseek-ai/dsh-mcp-client": ">=0.1.7-rc.2",
44
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.2",
45
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.2",
46
+ "@deepseek-ai/dsh-tools": ">=0.1.7-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",