dsh-vscode-mode 0.14.3 → 0.14.5

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/lib/index.js CHANGED
@@ -108,6 +108,7 @@ function guardMcp(ctx, exec) {
108
108
  const server = callServer(exec);
109
109
  const owners = serverOwners(ctx);
110
110
  const current = runtimeOf(ctx).workspace(exec.agent);
111
+ if (!RESOURCE_TOOLS.has(name) && [...mcpNames(ctx)].filter((key) => name.startsWith("mcp__" + key + "__")).length > 1) return "已拒绝:MCP 命名空间冲突,无法确认工具归属";
111
112
  if (!(RESOURCE_TOOLS.has(name) ? [...owners].filter(([key]) => key === server) : [...owners].filter(([key]) => name.startsWith("mcp__" + key + "__"))).some(([, owner]) => owner !== current)) return void 0;
112
113
  return current === void 0 ? "项目 MCP 需要在已注册工作区的对话中使用" : "已拒绝:当前对话工作区不能使用其他项目的 MCP";
113
114
  }
@@ -394,7 +395,7 @@ var McpRuntime = class {
394
395
  const update = async () => {
395
396
  this.projects.set(path, defs);
396
397
  this.changed();
397
- for (const [agent, record] of this.agents) await this.syncAgent(agent, record, refresh);
398
+ for (const [agent, record] of this.agents) await this.syncAgent(agent, record, this.workspace(agent) === path ? refresh : void 0);
398
399
  this.changed();
399
400
  };
400
401
  return this.enqueue(update);
@@ -477,7 +478,7 @@ var McpRuntime = class {
477
478
  const defs = this.projects.get(path ?? "") ?? /* @__PURE__ */ new Map();
478
479
  for (const [name, instance] of record.instances) {
479
480
  const def = defs.get(name);
480
- if (def?.enabled && instance.key === JSON.stringify(def.config) && name !== refresh) continue;
481
+ if (def?.enabled && instance.path === path && instance.key === JSON.stringify(def.config) && name !== refresh) continue;
481
482
  await this.stopOne(record, name);
482
483
  }
483
484
  for (const [name, def] of defs) {
@@ -487,6 +488,20 @@ var McpRuntime = class {
487
488
  }
488
489
  }
489
490
  /**
491
+ * Cache successful module loads while allowing explicit refresh after transient failures.
492
+ * @private @author ddj 2026年10月08号
493
+ * @returns Official plugin module; load failures propagate to the instance diagnostic.
494
+ */
495
+ async getModule() {
496
+ const loading = this.module ??= this.load();
497
+ try {
498
+ return await loading;
499
+ } catch (error) {
500
+ if (this.module === loading) this.module = void 0;
501
+ throw error;
502
+ }
503
+ }
504
+ /**
490
505
  * Mount the official plugin through agent.ctx; ACTIVE is not connection evidence.
491
506
  * @private @author ddj 2026年09月28号
492
507
  * @param agent Scoped context owner.
@@ -502,7 +517,7 @@ var McpRuntime = class {
502
517
  };
503
518
  record.instances.set(config.serverName, instance);
504
519
  try {
505
- const officialMcp = await (this.module ??= this.load());
520
+ const officialMcp = await this.getModule();
506
521
  if (!record.live || this.stopped) return;
507
522
  if (entriesOf(this.ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName)) throw new Error("serverName 已被全局 MCP 使用:" + config.serverName);
508
523
  const options = config.transport === "stdio" ? {
@@ -630,6 +645,7 @@ async function installMcpRuntime(ctx) {
630
645
  //#region src/mcpProject.ts
631
646
  /** Project MCP configuration; .mcp.json is the source of truth. @author ddj 2026年09月28号 */
632
647
  const FILE_NAME = ".mcp.json";
648
+ const projectStates = /* @__PURE__ */ new WeakMap();
633
649
  /**
634
650
  * Require an exact registered workspace before any project file mutation.
635
651
  * @private @author ddj 2026年09月28号
@@ -671,7 +687,17 @@ function projectEntryId(workspacePath, serverName) {
671
687
  return PROJECT_ENTRY_PREFIX + hashWorkspace(workspacePath) + "." + serverName;
672
688
  }
673
689
  /**
674
- * Read the complete source document; only ENOENT denotes an absent configuration.
690
+ * Identify an absent target using official host and legacy Node filesystem codes.
691
+ * @private @author ddj 2026年10月08号
692
+ * @param error Filesystem failure; messages and class identity are not authoritative.
693
+ * @returns Whether the backend explicitly reports a missing target.
694
+ */
695
+ function isMissingFile(error) {
696
+ const code = error?.code;
697
+ return code === "FS_NOT_FOUND" || code === "ENOENT";
698
+ }
699
+ /**
700
+ * Read the complete source document; only missing-target codes denote an absent configuration.
675
701
  * @private @author ddj 2026年09月28号
676
702
  * @param ctx Host filesystem.
677
703
  * @param workspacePath Registered workspace path.
@@ -685,7 +711,7 @@ async function readProjectJson(ctx, workspacePath) {
685
711
  try {
686
712
  text = await fs.readText(target);
687
713
  } catch (error) {
688
- if (error?.code === "ENOENT") return {};
714
+ if (isMissingFile(error)) return {};
689
715
  throw new Error(".mcp.json 读取失败:" + String(error));
690
716
  }
691
717
  let data;
@@ -723,7 +749,17 @@ function serversOf(data) {
723
749
  */
724
750
  function configFromDef(def, serverName) {
725
751
  if (!def) return null;
726
- const transport = typeof def.url === "string" ? "streamable-http" : "stdio";
752
+ if (def.transport !== void 0 && !["stdio", "streamable-http"].includes(def.transport)) throw new Error("不支持的 MCP transport:" + serverName);
753
+ if (def.args !== void 0 && (!Array.isArray(def.args) || def.args.some((arg) => typeof arg !== "string"))) throw new Error("MCP args 必须是字符串数组:" + serverName);
754
+ for (const field of ["env", "headers"]) {
755
+ const values = def[field];
756
+ if (values !== void 0 && (!values || typeof values !== "object" || Array.isArray(values) || Object.values(values).some((value) => value === null || ![
757
+ "string",
758
+ "number",
759
+ "boolean"
760
+ ].includes(typeof value)))) throw new Error("MCP " + field + " 必须是标量字典:" + serverName);
761
+ }
762
+ const transport = def.transport ?? (typeof def.url === "string" ? "streamable-http" : "stdio");
727
763
  const config = {
728
764
  serverName,
729
765
  transport
@@ -766,15 +802,19 @@ async function writeProjectJson(ctx, workspacePath, data) {
766
802
  * @returns Completion; throws on collision or an unreadable competing source document.
767
803
  */
768
804
  async function checkMcpName(ctx, serverName, workspacePath) {
769
- const conflict = "serverName \"" + serverName + "\" 已被另一个 MCP 使用(全局或其他项目),请换一个名称";
805
+ /** @private @author ddj 2026年10月08号 @param other Existing name. @param sameOwner Whether updates share ownership. @throws Error for ambiguous namespaces. */
806
+ const checkName = (other, sameOwner) => {
807
+ if (nameOverlap(serverName, other)) throw new Error("MCP 命名空间冲突:" + serverName + " / " + other + ",请避免 __ 前缀交叠");
808
+ if (other === serverName && !sameOwner) throw new Error("serverName \"" + serverName + "\" 已被另一个 MCP 使用(全局或其他项目),请换一个名称");
809
+ };
770
810
  for (const entry of entriesOf(ctx)) {
771
- if (isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) || workspacePath === void 0) continue;
772
- if (entry.options?.config?.serverName === serverName) throw new Error(conflict);
811
+ if (isProjectEntryId(String(entry.id ?? entry.options?.id ?? ""))) continue;
812
+ const name = entry.options?.config?.serverName;
813
+ if (typeof name === "string") checkName(name, workspacePath === void 0);
773
814
  }
774
815
  for (const ws of ctx.get("workspaceRegistry")?.list?.() ?? []) {
775
- if (ws.path === workspacePath) continue;
776
816
  const servers = serversOf(await readProjectJson(ctx, ws.path));
777
- if (Object.hasOwn(servers, serverName)) throw new Error(conflict);
817
+ for (const name of Object.keys(servers)) checkName(name, ws.path === workspacePath);
778
818
  }
779
819
  }
780
820
  /**
@@ -818,9 +858,16 @@ async function projectOf(ctx, workspacePath, title) {
818
858
  missingDir = !info || info.type !== "directory";
819
859
  if (!missingDir) await reconcileProject(ctx, workspacePath);
820
860
  } catch (error) {
821
- missingDir = error?.code === "ENOENT" || void 0;
861
+ missingDir = isMissingFile(error) || void 0;
822
862
  fileError = String(error);
823
863
  }
864
+ const owner = ctx.root ?? ctx;
865
+ const states = projectStates.get(owner) ?? /* @__PURE__ */ new Map();
866
+ states.set(workspacePath, {
867
+ missingDir,
868
+ fileError
869
+ });
870
+ projectStates.set(owner, states);
824
871
  return {
825
872
  workspacePath,
826
873
  title,
@@ -842,6 +889,26 @@ async function listProjects(ctx) {
842
889
  return { projects };
843
890
  }
844
891
  /**
892
+ * Read the current ledger only; polling never reconciles files or starts connections.
893
+ * @public @author ddj 2026年10月08号
894
+ * @param ctx Host services.
895
+ * @returns Masked global/project snapshots with last configuration diagnostics.
896
+ */
897
+ function mcpSnapshot(ctx) {
898
+ const states = projectStates.get(ctx.root ?? ctx);
899
+ const projects = (ctx.get("workspaceRegistry")?.list?.() ?? []).map((ws) => ({
900
+ workspacePath: ws.path,
901
+ title: ws.title ?? "",
902
+ source: "project",
903
+ servers: runtimeOf(ctx).views(ws.path),
904
+ ...states?.get(ws.path)
905
+ }));
906
+ return {
907
+ ...listMcp(ctx),
908
+ projects
909
+ };
910
+ }
911
+ /**
845
912
  * Merge editable fields while retaining advanced fields and unchanged masked credentials.
846
913
  * @private @author ddj 2026年09月28号
847
914
  * @param previous Persisted server definition.
@@ -849,20 +916,10 @@ async function listProjects(ctx) {
849
916
  * @returns Enabled source definition without runtime-only keys or stale transport fields.
850
917
  */
851
918
  function mergeDefinition(previous, config) {
852
- const next = mergeSecrets({
853
- ...previous,
854
- ...config
855
- }, previous);
919
+ const next = mergeMcp(previous, config);
856
920
  delete next.serverName;
857
921
  delete next.transport;
858
922
  delete next.disabled;
859
- const stale = config.transport === "stdio" ? ["url", "headers"] : [
860
- "command",
861
- "args",
862
- "cwd",
863
- "env"
864
- ];
865
- for (const key of stale) delete next[key];
866
923
  return next;
867
924
  }
868
925
  /**
@@ -875,6 +932,10 @@ function mergeDefinition(previous, config) {
875
932
  * @returns Updated project view; invalid sources or conflicts reject before writing.
876
933
  */
877
934
  async function projectSave(ctx, workspacePath, serverName, config) {
935
+ return mcpEdit(ctx, () => saveProject(ctx, workspacePath, serverName, config));
936
+ }
937
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @param config Editable fields. @returns Persisted project status. */
938
+ async function saveProject(ctx, workspacePath, serverName, config) {
878
939
  const workspace = requireWorkspace$2(ctx, workspacePath);
879
940
  validateConfig({
880
941
  ...config,
@@ -903,6 +964,10 @@ async function projectSave(ctx, workspacePath, serverName, config) {
903
964
  * @returns Updated project view; write failures leave active configuration intact.
904
965
  */
905
966
  async function projectRemove(ctx, workspacePath, serverName) {
967
+ return mcpEdit(ctx, () => removeProject(ctx, workspacePath, serverName));
968
+ }
969
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @returns Project after persisted removal. */
970
+ async function removeProject(ctx, workspacePath, serverName) {
906
971
  const workspace = requireWorkspace$2(ctx, workspacePath);
907
972
  const data = await readProjectJson(ctx, workspacePath);
908
973
  const servers = serversOf(data);
@@ -924,6 +989,10 @@ async function projectRemove(ctx, workspacePath, serverName) {
924
989
  * @returns Updated project view, including retained disabled definitions.
925
990
  */
926
991
  async function projectToggle(ctx, workspacePath, serverName, enabled) {
992
+ return mcpEdit(ctx, () => toggleProject(ctx, workspacePath, serverName, enabled));
993
+ }
994
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @param enabled Desired state. @returns Project after persisted toggle. */
995
+ async function toggleProject(ctx, workspacePath, serverName, enabled) {
927
996
  const workspace = requireWorkspace$2(ctx, workspacePath);
928
997
  const data = await readProjectJson(ctx, workspacePath);
929
998
  const servers = serversOf(data);
@@ -947,6 +1016,10 @@ async function projectToggle(ctx, workspacePath, serverName, enabled) {
947
1016
  * @returns Updated project view; disabled or idle definitions stay connection-free.
948
1017
  */
949
1018
  async function projectRefresh(ctx, workspacePath, serverName) {
1019
+ return mcpEdit(ctx, () => refreshProject(ctx, workspacePath, serverName));
1020
+ }
1021
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @returns Refreshed project status without racing edits. */
1022
+ async function refreshProject(ctx, workspacePath, serverName) {
950
1023
  const workspace = requireWorkspace$2(ctx, workspacePath);
951
1024
  const data = await readProjectJson(ctx, workspacePath);
952
1025
  if (!Object.hasOwn(serversOf(data), serverName)) throw new Error("项目中不存在该 MCP");
@@ -961,6 +1034,44 @@ async function projectRefresh(ctx, workspacePath, serverName) {
961
1034
  * 身份常量(包名/条目前缀/新旧格式判定)统一由 compat 层持有,本模块再导出保持既有 import 面。
962
1035
  * 作者 ddj 2026年08月22号
963
1036
  */
1037
+ /**
1038
+ * Compare the official tool-name namespaces without guessing the longest prefix.
1039
+ * @public @author ddj 2026年10月08号
1040
+ * @param left First server name.
1041
+ * @param right Second server name.
1042
+ * @returns Whether distinct names generate overlapping public tool names.
1043
+ */
1044
+ function nameOverlap(left, right) {
1045
+ return left !== right && (left.startsWith(right + "__") || right.startsWith(left + "__"));
1046
+ }
1047
+ /**
1048
+ * Collect known global and project namespaces without opening connections.
1049
+ * @public @author ddj 2026年10月08号
1050
+ * @param ctx Host context.
1051
+ * @returns Distinct server names used for fail-closed attribution.
1052
+ */
1053
+ function mcpNames(ctx) {
1054
+ const names = new Set(runtimeOf(ctx).owners().keys());
1055
+ for (const entry of entriesOf(ctx)) {
1056
+ const name = entry.options?.config?.serverName;
1057
+ if (typeof name === "string") names.add(name);
1058
+ }
1059
+ return names;
1060
+ }
1061
+ const edits = /* @__PURE__ */ new WeakMap();
1062
+ /**
1063
+ * Serialize profile/project edits together to preserve files and cross-scope name reservations.
1064
+ * @public @author ddj 2026年10月08号
1065
+ * @param ctx Host owner.
1066
+ * @param action Validated configuration operation.
1067
+ * @returns Operation result; a rejection never poisons the next edit.
1068
+ */
1069
+ function mcpEdit(ctx, action) {
1070
+ const owner = ctx.root ?? ctx;
1071
+ const next = (edits.get(owner) ?? Promise.resolve()).then(action, action);
1072
+ edits.set(owner, next);
1073
+ return next;
1074
+ }
964
1075
  /** 列出当前 loader 中的 MCP entry。 */
965
1076
  function entriesOf(ctx) {
966
1077
  const loader = ctx.get("loader");
@@ -986,7 +1097,10 @@ function toolsOf(ctx, serverName, agent) {
986
1097
  const visible = (ctx.get("tools")?.view?.(agent))?.visible;
987
1098
  if (!(visible instanceof Map)) return [];
988
1099
  const prefix = `mcp__${serverName}__`;
989
- return [...visible.entries()].filter(([name]) => String(name).startsWith(prefix)).map(([name, definition]) => ({
1100
+ const namespaces = mcpNames(ctx);
1101
+ namespaces.add(serverName);
1102
+ const belongs = (name) => name.startsWith(prefix) && [...namespaces].filter((server) => name.startsWith("mcp__" + server + "__")).length === 1;
1103
+ return [...visible.entries()].filter(([name]) => belongs(String(name))).map(([name, definition]) => ({
990
1104
  name: String(name).slice(prefix.length),
991
1105
  description: typeof definition?.description === "string" ? definition.description : void 0
992
1106
  }));
@@ -1018,6 +1132,27 @@ function mergeSecrets(next, previous) {
1018
1132
  return merged;
1019
1133
  }
1020
1134
  /**
1135
+ * Merge editable transport fields while retaining advanced settings and secret values.
1136
+ * @public @author ddj 2026年10月08号
1137
+ * @param previous Last saved configuration.
1138
+ * @param config Submitted transport configuration.
1139
+ * @returns Detached configuration without fields belonging to the old transport.
1140
+ */
1141
+ function mergeMcp(previous, config) {
1142
+ const next = mergeSecrets({
1143
+ ...previous,
1144
+ ...config
1145
+ }, previous);
1146
+ const stale = config.transport === "stdio" ? ["url", "headers"] : [
1147
+ "command",
1148
+ "args",
1149
+ "cwd",
1150
+ "env"
1151
+ ];
1152
+ for (const key of stale) delete next[key];
1153
+ return next;
1154
+ }
1155
+ /**
1021
1156
  * Summarize a global entry without treating plugin activation as a successful handshake.
1022
1157
  * @public @author ddj 2026年09月28号
1023
1158
  * @param ctx Host services.
@@ -1042,11 +1177,42 @@ function serverOf(ctx, entry) {
1042
1177
  error: state === 3 ? "MCP 插件未正常运行" : void 0
1043
1178
  };
1044
1179
  }
1045
- /** 校验 MCP 配置。 */
1180
+ /**
1181
+ * Validate transport fields before any loader or source mutation.
1182
+ * @public @author ddj 2026年10月08号
1183
+ * @param config Submitted configuration.
1184
+ * @throws Error for unsupported transport or malformed endpoint/arguments/maps.
1185
+ */
1046
1186
  function validateConfig(config) {
1047
- if (!/^[A-Za-z0-9_-]{1,32}$/.test(config.serverName)) throw new Error("serverName 只能包含字母、数字、下划线和连字符(最多 32 位)");
1048
- if (config.transport === "stdio" && !config.command?.trim()) throw new Error("stdio MCP 必须填写 command");
1049
- if (config.transport === "streamable-http" && !/^https?:\/\//.test(config.url ?? "")) throw new Error("HTTP MCP 必须填写 http(s) URL");
1187
+ if (!config || typeof config !== "object") throw new Error("MCP 配置必须是对象");
1188
+ if (typeof config.serverName !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(config.serverName)) throw new Error("serverName 只能包含字母、数字、下划线和连字符(最多 32 位)");
1189
+ if (!["stdio", "streamable-http"].includes(config.transport)) throw new Error("不支持的 MCP transport");
1190
+ if (config.transport === "stdio") {
1191
+ if (typeof config.command !== "string" || !config.command.trim()) throw new Error("stdio MCP 必须填写 command");
1192
+ if (config.args !== void 0 && (!Array.isArray(config.args) || config.args.some((arg) => typeof arg !== "string"))) throw new Error("MCP args 必须是字符串数组");
1193
+ if (config.cwd !== void 0 && typeof config.cwd !== "string") throw new Error("MCP cwd 必须是字符串");
1194
+ validatePairs(config.env, "env");
1195
+ } else {
1196
+ let url;
1197
+ try {
1198
+ url = new URL(config.url ?? "");
1199
+ } catch {
1200
+ throw new Error("HTTP MCP 必须填写有效 http(s) URL");
1201
+ }
1202
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) throw new Error("HTTP MCP 必须填写有效 http(s) URL,凭据请使用请求头");
1203
+ validatePairs(config.headers, "headers");
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Validate an optional string-valued map without coercing malformed input.
1208
+ * @private @author ddj 2026年10月08号
1209
+ * @param values Submitted map.
1210
+ * @param field Field used in the diagnostic.
1211
+ * @throws Error for non-object or non-string values.
1212
+ */
1213
+ function validatePairs(values, field) {
1214
+ if (values === void 0) return;
1215
+ if (!values || typeof values !== "object" || Array.isArray(values) || Object.entries(values).some(([key, value]) => !key || typeof value !== "string")) throw new Error("MCP " + field + " 必须是字符串字典");
1050
1216
  }
1051
1217
  /** 列出全局(profile)MCP 服务,过滤掉新旧格式的项目级条目。 */
1052
1218
  function listMcp(ctx) {
@@ -1060,16 +1226,16 @@ function listMcp(ctx) {
1060
1226
  * @returns Masked server status; project name collisions reject before writing.
1061
1227
  */
1062
1228
  async function saveMcp(ctx, config) {
1229
+ return mcpEdit(ctx, () => saveGlobal(ctx, config));
1230
+ }
1231
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param config Configuration. @returns Saved status; validation failures do not write. */
1232
+ async function saveGlobal(ctx, config) {
1063
1233
  validateConfig(config);
1064
1234
  await checkMcpName(ctx, config.serverName);
1065
1235
  const loader = ctx.get("loader");
1066
1236
  if (!loader) throw new Error("缺少 loader 服务");
1067
1237
  const existing = entriesOf(ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName);
1068
- const previous = existing?.options?.config ?? {};
1069
- const next = mergeSecrets({
1070
- ...previous,
1071
- ...config
1072
- }, previous);
1238
+ const next = mergeMcp(existing?.options?.config ?? {}, config);
1073
1239
  if (existing) {
1074
1240
  await loader.update(existing.id, { config: next });
1075
1241
  return serverOf(ctx, loader.resolve(existing.id));
@@ -1080,31 +1246,46 @@ async function saveMcp(ctx, config) {
1080
1246
  });
1081
1247
  return serverOf(ctx, loader.resolve(id));
1082
1248
  }
1083
- /** 删除 MCP 服务。 */
1084
- async function removeMcp(ctx, id) {
1249
+ /** @private @author ddj 2026年10月08号 @param ctx Host services. @param id Global identity. @returns Loader and verified global entry. @throws Missing service, entry or wrong scope. */
1250
+ function globalEntry(ctx, id) {
1085
1251
  const loader = ctx.get("loader");
1086
1252
  if (!loader) throw new Error("缺少 loader 服务");
1087
1253
  const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1088
1254
  if (!entry) throw new Error("MCP 服务不存在");
1089
- await loader.remove(entry.id);
1255
+ if (isProjectEntryId(String(entry.id ?? entry.options?.id ?? ""))) throw new Error("项目 MCP 必须通过项目入口操作");
1256
+ return {
1257
+ loader,
1258
+ entry
1259
+ };
1260
+ }
1261
+ /** @public @author ddj 2026年10月08号 @param ctx Host services. @param id Global identity. @returns Persisted removal, serialized with all settings writes. */
1262
+ async function removeMcp(ctx, id) {
1263
+ /** @private @author ddj 2026年10月08号 @returns Loader removal after scope validation. */
1264
+ const remove = async () => {
1265
+ const { loader, entry } = globalEntry(ctx, id);
1266
+ await loader.remove(entry.id);
1267
+ };
1268
+ return mcpEdit(ctx, remove);
1090
1269
  }
1091
- /** 切换 MCP 服务启用状态。 */
1270
+ /** @public @author ddj 2026年10月08号 @param ctx Host services. @param id Global identity. @param enabled Desired state. @returns Persisted status after serialized toggle. */
1092
1271
  async function toggleMcp(ctx, id, enabled) {
1093
- const loader = ctx.get("loader");
1094
- if (!loader) throw new Error("缺少 loader 服务");
1095
- const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1096
- if (!entry) throw new Error("MCP 服务不存在");
1097
- await loader.update(entry.id, { disabled: !enabled });
1098
- return serverOf(ctx, loader.resolve(entry.id));
1272
+ /** @private @author ddj 2026年10月08号 @returns Loader status after toggle. */
1273
+ const toggle = async () => {
1274
+ const { loader, entry } = globalEntry(ctx, id);
1275
+ await loader.update(entry.id, { disabled: !enabled });
1276
+ return serverOf(ctx, loader.resolve(entry.id));
1277
+ };
1278
+ return mcpEdit(ctx, toggle);
1099
1279
  }
1100
- /** 重新加载 MCP 服务。 */
1280
+ /** @public @author ddj 2026年10月08号 @param ctx Host services. @param id Global identity. @returns Loader status after serialized refresh. */
1101
1281
  async function refreshMcp(ctx, id) {
1102
- const loader = ctx.get("loader");
1103
- if (!loader) throw new Error("缺少 loader 服务");
1104
- const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1105
- if (!entry) throw new Error("MCP 服务不存在");
1106
- await loader.update(entry.id, { config: { ...entry.options?.config ?? {} } });
1107
- return serverOf(ctx, loader.resolve(entry.id));
1282
+ /** @private @author ddj 2026年10月08号 @returns Loader status after refresh. */
1283
+ const refresh = async () => {
1284
+ const { loader, entry } = globalEntry(ctx, id);
1285
+ await loader.update(entry.id, { config: { ...entry.options?.config ?? {} } });
1286
+ return serverOf(ctx, loader.resolve(entry.id));
1287
+ };
1288
+ return mcpEdit(ctx, refresh);
1108
1289
  }
1109
1290
  //#endregion
1110
1291
  //#region src/shared/keybindings.ts
@@ -2942,19 +3123,23 @@ function inDshRange(version, range) {
2942
3123
  }
2943
3124
  /**
2944
3125
  * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
2945
- * 逐线自新到旧匹配,先命中先返回;仅 0.2.0-rc.1 精确标记为已审计版本。
2946
- * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号
3126
+ * 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2 精确标记。
3127
+ * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号
2947
3128
  * @param input 版本串(如 '0.1.6-alpha.2')
2948
3129
  * @returns 版本线标签
2949
3130
  */
2950
3131
  function familyLabel(input) {
2951
3132
  const version = parseDshVersion(input);
2952
3133
  if (!version) return "未知";
3134
+ if (inDshRange(version, {
3135
+ from: "0.2.0-rc.2",
3136
+ to: "0.2.0-rc.2"
3137
+ })) return "0.2.0-rc.2(设置=profile Config;按能力探测)";
2953
3138
  if (inDshRange(version, {
2954
3139
  from: "0.2.0-rc.1",
2955
3140
  to: "0.2.0-rc.1"
2956
3141
  })) return "0.2.0-rc.1(设置=profile Config;按能力探测)";
2957
- if (inDshRange(version, { from: "0.2.0-0" })) return "0.2.0 及更新(除 rc.1 外未经逐版适配;按能力探测)";
3142
+ if (inDshRange(version, { from: "0.2.0-0" })) return "0.2.0 及更新(除 rc.1/rc.2 外未经逐版适配;按能力探测)";
2958
3143
  if (inDshRange(version, { from: "0.1.7-alpha.1" })) return "0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)";
2959
3144
  if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
2960
3145
  if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
@@ -12162,6 +12347,10 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
12162
12347
  ok: true,
12163
12348
  ...listMcp(ctx)
12164
12349
  }),
12350
+ "mcp.snapshot": async () => ({
12351
+ ok: true,
12352
+ ...mcpSnapshot(ctx)
12353
+ }),
12165
12354
  "mcp.save": async (args) => {
12166
12355
  try {
12167
12356
  return {