@huanlin/dsh-plugin-mcp-manager 0.1.0 → 0.1.2

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.
Files changed (4) hide show
  1. package/README.md +24 -11
  2. package/lib/index.js +1229 -268
  3. package/lib/index.mjs +147 -13
  4. package/package.json +1 -1
package/lib/index.mjs CHANGED
@@ -132,7 +132,7 @@ function configToData(config) {
132
132
  }
133
133
  /**
134
134
  * 读全部 MCP 服务器行。配置块缺失/结构异常的行被跳过(不抛——读路径容忍)。
135
- * 返回每行的 id 与解析出的 config(仅含存在的字段)。
135
+ * 返回每行的 id 与解析出的 config(仅含存在的字段)+ entry-level disabled 标志。
136
136
  */
137
137
  function listServers() {
138
138
  const doc = readPatchDocument();
@@ -147,10 +147,13 @@ function listServers() {
147
147
  } catch {
148
148
  continue;
149
149
  }
150
+ const disabledRaw = row.get("disabled");
151
+ const disabled = disabledRaw === true || disabledRaw === "true";
150
152
  out.push({
151
153
  id,
152
154
  name: MCP_CLIENT_PACKAGE,
153
- config
155
+ config,
156
+ disabled
154
157
  });
155
158
  }
156
159
  return out;
@@ -277,10 +280,29 @@ function deleteServer(id, opts) {
277
280
  console.log(`[dsh-mcp-manager] deleted server ${id}`);
278
281
  return true;
279
282
  }
283
+ /**
284
+ * 设置一个服务器的禁用状态。`disabled: true` 写在 entry level(与 id/name/config
285
+ * 同级),loader 跳过挂载该 mcp-client 实例 → 工具不注册(模型不可见),配置保留。
286
+ * 设为 false 时移除 `disabled` 字段(比 `disabled: false` 更干净),loader HMR
287
+ * 重新挂载实例。
288
+ *
289
+ * @returns true 设置成功;false 行不存在。
290
+ * @throws RegistryError 写后校验失败(已回滚)。
291
+ */
292
+ function setServerDisabled(id, disabled, opts) {
293
+ const doc = readPatchDocument();
294
+ const target = collectMcpRows(doc).find((r) => r.id === id);
295
+ if (target === void 0) return false;
296
+ if (disabled) target.row.set("disabled", true);
297
+ else if (target.row.has("disabled")) target.row.delete("disabled");
298
+ writeAndValidate(doc, opts?.loadOverlayPatches);
299
+ console.log(`[dsh-mcp-manager] set server ${id} disabled=${disabled}`);
300
+ return true;
301
+ }
280
302
  //#endregion
281
303
  //#region src/tools.ts
282
304
  /**
283
- * MCP 管理工具(mcp_* ×4):agent 面的服务器注册表管理(对齐开发计划 §M4)。
305
+ * MCP 管理工具(mcp_* ×5):agent 面的服务器注册表管理(对齐开发计划 §M4)。
284
306
  * 与 GUI 面板写同一安装态(profile cordis.patch.yml 的 mcp-client insert 行),
285
307
  * 配置 HMR 实时挂载——agent 调用后工具立即可用(若服务器连接成功)。
286
308
  *
@@ -288,6 +310,8 @@ function deleteServer(id, opts) {
288
310
  * - mcp_server_add:新增服务器(校验 + 写 insert 行 → HMR 挂载 mcp-client 实例)
289
311
  * - mcp_server_update:整块替换 config(serverName 不变则工具名不变)
290
312
  * - mcp_server_remove:移除行 → 工具随实例 dispose 注销
313
+ * - mcp_server_set_enabled:禁用/启用 entry-level disabled 字段(config 保留,
314
+ * 禁用时 loader 跳过挂载 → 工具不注册 → 模型不可见)
291
315
  *
292
316
  * 依赖注入(deps):避免与 index.ts 循环依赖。连接生命周期完全委托官方
293
317
  * mcp-client——管理插件只写配置,不拉连接。
@@ -295,10 +319,11 @@ function deleteServer(id, opts) {
295
319
  /** 把注册表行 + 运行态工具名投影为 agent 可见的规范视图。 */
296
320
  function toServerView(row, toolNames, connectingSince) {
297
321
  const prefix = `mcp__${row.config.serverName}__`;
298
- const toolCount = toolNames.filter((n) => n.startsWith(prefix)).length;
322
+ const toolCount = row.disabled ? 0 : toolNames.filter((n) => n.startsWith(prefix)).length;
299
323
  const endpoint = row.config.transport === "stdio" ? `${row.config.command ?? ""} ${(row.config.args ?? []).join(" ")}`.trim() : row.config.url ?? "";
300
324
  let status;
301
- if (toolCount > 0) status = "connected";
325
+ if (row.disabled) status = "disabled";
326
+ else if (toolCount > 0) status = "connected";
302
327
  else {
303
328
  const since = connectingSince.get(row.config.serverName);
304
329
  status = since !== void 0 && Date.now() - since < 3e4 ? "connecting" : "disconnected";
@@ -309,6 +334,7 @@ function toServerView(row, toolNames, connectingSince) {
309
334
  transport: row.config.transport,
310
335
  endpoint,
311
336
  toolCount,
337
+ disabled: row.disabled,
312
338
  status
313
339
  };
314
340
  }
@@ -390,7 +416,7 @@ function createMcpTools(deps) {
390
416
  return [
391
417
  defineTool({
392
418
  name: "mcp_server_list",
393
- description: "List registered MCP servers and their live tool counts. Each server is an @deepseek-ai/dsh-mcp-client instance mounted from the profile cordis.patch.yml insert row. status: connected (tools registered), connecting (mcp-client mounting/handshaking within the post-write grace window), disconnected (0 tools past the grace window — failed/exhausted).",
419
+ description: "List registered MCP servers and their live tool counts. Each server is an @deepseek-ai/dsh-mcp-client instance mounted from the profile cordis.patch.yml insert row. status: connected (tools registered), connecting (mcp-client mounting/handshaking within the post-write grace window), disconnected (0 tools past the grace window — failed/exhausted), disabled (entry-level disabled=true — config preserved but loader skips mounting, tools not registered and invisible to the model).",
394
420
  parameters: {},
395
421
  output: {
396
422
  schema: {
@@ -423,13 +449,18 @@ function createMcpTools(deps) {
423
449
  type: "number",
424
450
  required: true
425
451
  },
452
+ disabled: {
453
+ type: "boolean",
454
+ required: true
455
+ },
426
456
  status: {
427
457
  type: "string",
428
458
  required: true,
429
459
  enum: [
430
460
  "connected",
431
461
  "connecting",
432
- "disconnected"
462
+ "disconnected",
463
+ "disabled"
433
464
  ]
434
465
  }
435
466
  }
@@ -568,6 +599,61 @@ function createMcpTools(deps) {
568
599
  message: `mcp_server_remove: removed "${id}" — mcp-client disposing, tools unregistering.`
569
600
  };
570
601
  }
602
+ }),
603
+ defineTool({
604
+ name: "mcp_server_set_enabled",
605
+ description: "Enable or disable an MCP server without deleting its config. Sets the entry-level `disabled` field (sibling of id/name/config) in the profile cordis.patch.yml. When disabled, the loader skips mounting the @deepseek-ai/dsh-mcp-client instance — its tools (mcp__<serverName>__*) are NOT registered and the model CANNOT see or call them. The config block is preserved verbatim, so re-enabling is a no-cost toggle (no re-entry of fields). Use this instead of mcp_server_remove when you want to temporarily hide a server from the model.",
606
+ parameters: {
607
+ id: {
608
+ type: "string",
609
+ required: true,
610
+ description: "The server insert-row id (e.g. mcp-github)."
611
+ },
612
+ enabled: {
613
+ type: "boolean",
614
+ required: true,
615
+ description: "true = mount the instance (tools visible); false = skip mounting (tools hidden, config preserved)."
616
+ }
617
+ },
618
+ output: {
619
+ schema: {
620
+ type: "object",
621
+ additionalProperties: false,
622
+ properties: {
623
+ ok: {
624
+ type: "boolean",
625
+ required: true
626
+ },
627
+ id: {
628
+ type: "string",
629
+ required: true
630
+ },
631
+ enabled: {
632
+ type: "boolean",
633
+ required: true
634
+ },
635
+ message: {
636
+ type: "string",
637
+ required: true
638
+ }
639
+ }
640
+ },
641
+ render: (_args, value) => [{
642
+ type: "text",
643
+ text: value.message
644
+ }]
645
+ },
646
+ async execute(args) {
647
+ const id = String(args.id);
648
+ const enabled = Boolean(args.enabled);
649
+ deps.setServerDisabled(id, !enabled);
650
+ return {
651
+ ok: true,
652
+ id,
653
+ enabled,
654
+ message: enabled ? `mcp_server_set_enabled: enabled "${id}" — mcp-client mounting via config HMR, tools will appear as mcp__<serverName>__* once the connection succeeds.` : `mcp_server_set_enabled: disabled "${id}" — mcp-client disposing, tools unregistering. Config preserved in patch file; re-enable anytime.`
655
+ };
656
+ }
571
657
  })
572
658
  ];
573
659
  }
@@ -599,8 +685,9 @@ function readBody(req) {
599
685
  * 这是 P0 朴素推断(对齐开发计划 §4),非真实连接状态。
600
686
  */
601
687
  const CONNECTING_GRACE_MS = 3e4;
602
- /** 计算一个服务器的三态状态。 */
603
- function computeStatus(toolCount, serverName, connectingSince) {
688
+ /** 计算一个服务器的四态状态。 */
689
+ function computeStatus(toolCount, serverName, connectingSince, disabled) {
690
+ if (disabled) return "disabled";
604
691
  if (toolCount > 0) return "connected";
605
692
  const since = connectingSince.get(serverName);
606
693
  if (since !== void 0 && Date.now() - since < CONNECTING_GRACE_MS) return "connecting";
@@ -620,6 +707,10 @@ function apply(ctx) {
620
707
  const markConnecting = (serverName) => {
621
708
  connectingSince.set(serverName, Date.now());
622
709
  };
710
+ /** 清除连接中标记(禁用时调用——不再处于连接中,而是已禁用)。 */
711
+ const clearConnecting = (serverName) => {
712
+ connectingSince.delete(serverName);
713
+ };
623
714
  const mcpTools = createMcpTools({
624
715
  listServers,
625
716
  addServer: (config, opts) => {
@@ -635,6 +726,16 @@ function apply(ctx) {
635
726
  markConnecting(config.serverName);
636
727
  },
637
728
  deleteServer: (id) => deleteServer(id, { loadOverlayPatches }),
729
+ setServerDisabled: (id, disabled) => {
730
+ setServerDisabled(id, disabled, { loadOverlayPatches });
731
+ if (disabled) {
732
+ const row = listServers().find((r) => r.id === id);
733
+ if (row !== void 0) clearConnecting(row.config.serverName);
734
+ } else {
735
+ const row = listServers().find((r) => r.id === id);
736
+ if (row !== void 0) markConnecting(row.config.serverName);
737
+ }
738
+ },
638
739
  registeredToolNames: () => (ctx.tools?.schemas() ?? []).map((s) => s.name),
639
740
  connectingSince,
640
741
  selfId: "@huanlin/dsh-plugin-mcp-manager"
@@ -665,14 +766,15 @@ function apply(ctx) {
665
766
  ok: true,
666
767
  servers: listServers().map((row) => {
667
768
  const prefix = `mcp__${row.config.serverName}__`;
668
- const toolCount = toolNames.filter((n) => n.startsWith(prefix)).length;
769
+ const toolCount = row.disabled ? 0 : toolNames.filter((n) => n.startsWith(prefix)).length;
669
770
  return {
670
771
  id: row.id,
671
772
  serverName: row.config.serverName,
672
773
  transport: row.config.transport,
673
774
  endpoint: row.config.transport === "stdio" ? `${row.config.command ?? ""} ${(row.config.args ?? []).join(" ")}`.trim() : row.config.url ?? "",
674
775
  toolCount,
675
- status: computeStatus(toolCount, row.config.serverName, connectingSince),
776
+ disabled: row.disabled,
777
+ status: computeStatus(toolCount, row.config.serverName, connectingSince, row.disabled),
676
778
  config: row.config
677
779
  };
678
780
  })
@@ -713,12 +815,13 @@ function apply(ctx) {
713
815
  ok: true,
714
816
  status: listServers().map((row) => {
715
817
  const prefix = `mcp__${row.config.serverName}__`;
716
- const toolCount = toolNames.filter((n) => n.startsWith(prefix)).length;
818
+ const toolCount = row.disabled ? 0 : toolNames.filter((n) => n.startsWith(prefix)).length;
717
819
  return {
718
820
  id: row.id,
719
821
  serverName: row.config.serverName,
720
822
  toolCount,
721
- status: computeStatus(toolCount, row.config.serverName, connectingSince)
823
+ disabled: row.disabled,
824
+ status: computeStatus(toolCount, row.config.serverName, connectingSince, row.disabled)
722
825
  };
723
826
  })
724
827
  });
@@ -769,6 +872,37 @@ function apply(ctx) {
769
872
  });
770
873
  return;
771
874
  }
875
+ if (method === "PATCH" && putMatch !== null) {
876
+ const id = decodeURIComponent(putMatch[1]);
877
+ const body = await readBody(req);
878
+ const parsed = JSON.parse(body);
879
+ if (parsed.disabled === void 0 || typeof parsed.disabled !== "boolean") {
880
+ jsonRes(400, {
881
+ ok: false,
882
+ message: "disabled (boolean) is required"
883
+ });
884
+ return;
885
+ }
886
+ if (!setServerDisabled(id, parsed.disabled, { loadOverlayPatches })) {
887
+ jsonRes(404, {
888
+ ok: false,
889
+ message: `server "${id}" not found`
890
+ });
891
+ return;
892
+ }
893
+ const row = listServers().find((r) => r.id === id);
894
+ if (row !== void 0) {
895
+ if (parsed.disabled) connectingSince.delete(row.config.serverName);
896
+ else markConnecting(row.config.serverName);
897
+ }
898
+ jsonRes(200, {
899
+ ok: true,
900
+ id,
901
+ disabled: parsed.disabled,
902
+ message: `server ${id} ${parsed.disabled ? "disabled" : "enabled"} (config preserved)`
903
+ });
904
+ return;
905
+ }
772
906
  if (method === "DELETE" && putMatch !== null) {
773
907
  const id = decodeURIComponent(putMatch[1]);
774
908
  if (!deleteServer(id, { loadOverlayPatches })) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huanlin/dsh-plugin-mcp-manager",
3
3
  "description": "MCP 服务器管理面板:GUI 增删改 MCP 服务器配置(写入 profile cordis.patch.yml 的 mcp-client insert 行,配置 HMR 实时挂载)+ 工具浏览 + agent 面 mcp_* 管理工具。连接生命周期委托官方 @deepseek-ai/dsh-mcp-client。",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },