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/README.md +20 -9
- package/lib/client.js +1445 -400
- package/lib/client.js.map +1 -1
- package/lib/index.js +242 -53
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
- package/skills/dsh-vscodemode-mcp/SKILL.md +9 -3
- package/src/client/diffDockStore.ts +8 -3
- package/src/client/index.ts +2 -0
- package/src/client/mcpForm.ts +79 -0
- package/src/client/mcpState.ts +179 -0
- package/src/client/monaco/diffRender.ts +234 -174
- package/src/client/monaco/theme.ts +26 -15
- package/src/client/officialSidebar.ts +6 -1
- package/src/client/openReceipt.ts +13 -3
- package/src/client/planBridge.ts +178 -0
- package/src/client/planState.ts +129 -0
- package/src/client/receiptState.ts +1 -0
- package/src/client/state/regions.ts +83 -3
- package/src/client/styles/editor.css +8 -7
- package/src/client/styles/mcp.css +6 -3
- package/src/client/ui/ClaimRouter.ts +1 -1
- package/src/client/ui/EditorView.ts +107 -36
- package/src/client/ui/McpSettings.ts +174 -137
- package/src/client/ui/OfficialSideTab.ts +2 -2
- package/src/dshVersion.ts +4 -3
- package/src/mcp.ts +131 -23
- package/src/mcpIsolation.ts +2 -1
- package/src/mcpProject.ts +74 -14
- package/src/mcpRuntime.ts +18 -3
- package/src/rpc.ts +2 -1
- package/src/shared/mcpStatus.ts +30 -0
- package/src/shared/rpc.ts +2 -0
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? ""))
|
|
772
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
1048
|
-
if (config.
|
|
1049
|
-
if (
|
|
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
|
|
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
|
-
/**
|
|
1084
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
*
|
|
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 {
|