dsh-plugin-capabilities 0.3.7 → 0.3.9
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 +4 -2
- package/lib/client.js +158 -32
- package/lib/client.js.map +2 -2
- package/lib/index.js +245 -38
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/agents.test.ts +47 -1
- package/src/agents.ts +59 -8
- package/src/client/MarketTab.tsx +23 -7
- package/src/client/McpTab.tsx +128 -23
- package/src/client/index.ts +12 -8
- package/src/client/locales.ts +30 -8
- package/src/index.ts +2 -1
- package/src/mcp.test.ts +135 -2
- package/src/mcp.ts +156 -18
- package/src/profile.ts +6 -2
- package/src/routes.ts +116 -26
package/lib/index.js
CHANGED
|
@@ -667,14 +667,14 @@ function stringArray(value) {
|
|
|
667
667
|
const out = value.filter((entry) => typeof entry === "string");
|
|
668
668
|
return out.length > 0 ? out : void 0;
|
|
669
669
|
}
|
|
670
|
-
function
|
|
670
|
+
function mapMcpServersEntry(agent, name2, entry) {
|
|
671
671
|
if (typeof entry !== "object" || entry === null) return null;
|
|
672
672
|
const record = entry;
|
|
673
673
|
const type = typeof record.type === "string" ? record.type : "stdio";
|
|
674
674
|
if (type === "stdio" || type === "stdio" && record.command !== void 0) {
|
|
675
675
|
if (typeof record.command !== "string" || record.command === "") return null;
|
|
676
676
|
return {
|
|
677
|
-
agent
|
|
677
|
+
agent,
|
|
678
678
|
name: name2,
|
|
679
679
|
transport: "stdio",
|
|
680
680
|
command: record.command,
|
|
@@ -685,7 +685,7 @@ function mapClaudeEntry(name2, entry) {
|
|
|
685
685
|
if (type === "http" || type === "streamable-http") {
|
|
686
686
|
if (typeof record.url !== "string" || record.url === "") return null;
|
|
687
687
|
return {
|
|
688
|
-
agent
|
|
688
|
+
agent,
|
|
689
689
|
name: name2,
|
|
690
690
|
transport: "streamable-http",
|
|
691
691
|
url: record.url,
|
|
@@ -708,11 +708,57 @@ function scanClaudeMcp(home = homedir()) {
|
|
|
708
708
|
}
|
|
709
709
|
const out = [];
|
|
710
710
|
for (const [name2, entry] of Object.entries(merged)) {
|
|
711
|
-
const mapped =
|
|
711
|
+
const mapped = mapMcpServersEntry("claude-code", name2, entry);
|
|
712
712
|
if (mapped !== null) out.push(mapped);
|
|
713
713
|
}
|
|
714
714
|
return out;
|
|
715
715
|
}
|
|
716
|
+
function scanCursorMcp(home = homedir()) {
|
|
717
|
+
const file = join(home, ".cursor", "mcp.json");
|
|
718
|
+
if (!existsSync(file)) return [];
|
|
719
|
+
let parsed;
|
|
720
|
+
try {
|
|
721
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
722
|
+
} catch {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
if (typeof parsed.mcpServers !== "object" || parsed.mcpServers === null) return [];
|
|
726
|
+
const out = [];
|
|
727
|
+
for (const [name2, entry] of Object.entries(parsed.mcpServers)) {
|
|
728
|
+
const mapped = mapMcpServersEntry("cursor", name2, entry);
|
|
729
|
+
if (mapped !== null) out.push(mapped);
|
|
730
|
+
}
|
|
731
|
+
return out;
|
|
732
|
+
}
|
|
733
|
+
function scanGeminiMcp(home = homedir()) {
|
|
734
|
+
const file = join(home, ".gemini", "settings.json");
|
|
735
|
+
if (!existsSync(file)) return [];
|
|
736
|
+
let parsed;
|
|
737
|
+
try {
|
|
738
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
739
|
+
} catch {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
if (typeof parsed.mcpServers !== "object" || parsed.mcpServers === null) return [];
|
|
743
|
+
const out = [];
|
|
744
|
+
for (const [name2, entry] of Object.entries(parsed.mcpServers)) {
|
|
745
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
746
|
+
const record = entry;
|
|
747
|
+
if (typeof record.command === "string" && record.command !== "") {
|
|
748
|
+
out.push({
|
|
749
|
+
agent: "gemini",
|
|
750
|
+
name: name2,
|
|
751
|
+
transport: "stdio",
|
|
752
|
+
command: record.command,
|
|
753
|
+
args: stringArray(record.args),
|
|
754
|
+
env: stringEntries(record.env)
|
|
755
|
+
});
|
|
756
|
+
} else if (typeof record.httpUrl === "string" && record.httpUrl !== "") {
|
|
757
|
+
out.push({ agent: "gemini", name: name2, transport: "streamable-http", url: record.httpUrl });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
716
762
|
function scanCodexMcp(home = homedir()) {
|
|
717
763
|
const file = join(home, ".codex", "config.toml");
|
|
718
764
|
if (!existsSync(file)) return [];
|
|
@@ -745,7 +791,7 @@ function scanCodexMcp(home = homedir()) {
|
|
|
745
791
|
}
|
|
746
792
|
function scanAllMcp(home = homedir()) {
|
|
747
793
|
const seen = /* @__PURE__ */ new Set();
|
|
748
|
-
return [...scanClaudeMcp(home), ...scanCodexMcp(home)].filter((server) => {
|
|
794
|
+
return [...scanClaudeMcp(home), ...scanCodexMcp(home), ...scanCursorMcp(home), ...scanGeminiMcp(home)].filter((server) => {
|
|
749
795
|
const key = `${server.agent}/${server.name}`;
|
|
750
796
|
if (seen.has(key)) return false;
|
|
751
797
|
seen.add(key);
|
|
@@ -764,9 +810,11 @@ function argvProfile(argv = process.argv) {
|
|
|
764
810
|
if (flag !== -1 && flag + 1 < argv.length && !argv[flag + 1].startsWith("-")) return argv[flag + 1];
|
|
765
811
|
return void 0;
|
|
766
812
|
}
|
|
813
|
+
function dshHomeDir(dshHome = process.env.DSH_HOME) {
|
|
814
|
+
return dshHome ?? join2(homedir2(), ".dsh");
|
|
815
|
+
}
|
|
767
816
|
function profileDir(profile, dshHome = process.env.DSH_HOME) {
|
|
768
|
-
|
|
769
|
-
return join2(home, "profiles", profile);
|
|
817
|
+
return join2(dshHomeDir(dshHome), "profiles", profile);
|
|
770
818
|
}
|
|
771
819
|
|
|
772
820
|
// src/routes.ts
|
|
@@ -1437,8 +1485,8 @@ function assertPatchParses(path, doc) {
|
|
|
1437
1485
|
`\u914D\u7F6E\u6587\u4EF6 ${path} \u5B58\u5728 YAML \u8BED\u6CD5\u9519\u8BEF\uFF08${at}${total}\uFF09\uFF0C\u672A\u5199\u5165\u4EFB\u4F55\u5185\u5BB9\uFF0C\u8BF7\u4FEE\u590D\u8BE5\u6587\u4EF6\u540E\u91CD\u8BD5 / YAML syntax error in the patch file (${at}${total}); nothing was written \u2014 fix it and retry`
|
|
1438
1486
|
);
|
|
1439
1487
|
}
|
|
1440
|
-
function loadPatch(
|
|
1441
|
-
const path = join9(
|
|
1488
|
+
function loadPatch(dirPath) {
|
|
1489
|
+
const path = join9(dirPath, "cordis.patch.yml");
|
|
1442
1490
|
const text = existsSync6(path) ? readFileSync6(path, "utf8") : "[]";
|
|
1443
1491
|
const doc = parseDocument(text);
|
|
1444
1492
|
assertPatchParses(path, doc);
|
|
@@ -1446,9 +1494,9 @@ function loadPatch(profileDirPath) {
|
|
|
1446
1494
|
if (contents !== null && contents.flow === true && contents.items.length === 0) contents.flow = false;
|
|
1447
1495
|
return doc;
|
|
1448
1496
|
}
|
|
1449
|
-
function savePatch(
|
|
1450
|
-
mkdirSync5(
|
|
1451
|
-
writeFileSync4(join9(
|
|
1497
|
+
function savePatch(dirPath, doc) {
|
|
1498
|
+
mkdirSync5(dirPath, { recursive: true });
|
|
1499
|
+
writeFileSync4(join9(dirPath, "cordis.patch.yml"), String(doc), "utf8");
|
|
1452
1500
|
}
|
|
1453
1501
|
function toNode(value) {
|
|
1454
1502
|
return new Document(value).contents;
|
|
@@ -1532,10 +1580,36 @@ function takenIds(doc) {
|
|
|
1532
1580
|
}
|
|
1533
1581
|
return taken;
|
|
1534
1582
|
}
|
|
1535
|
-
function listMcp(
|
|
1536
|
-
const doc = loadPatch(
|
|
1583
|
+
function listMcp(dirPath) {
|
|
1584
|
+
const doc = loadPatch(dirPath);
|
|
1537
1585
|
return mcpRowItems(doc).map(({ node }) => rowToMcp(doc, node));
|
|
1538
1586
|
}
|
|
1587
|
+
function listMcpScoped(profileDirPath, dshHomePath) {
|
|
1588
|
+
let globalRows = [];
|
|
1589
|
+
let globalError;
|
|
1590
|
+
if (existsSync6(join9(dshHomePath, "cordis.patch.yml"))) {
|
|
1591
|
+
try {
|
|
1592
|
+
globalRows = listMcp(dshHomePath);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
globalError = error instanceof Error ? error.message : String(error);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const globalIds = new Set(globalRows.map((row) => row.id).filter((id) => id !== ""));
|
|
1598
|
+
return {
|
|
1599
|
+
servers: [
|
|
1600
|
+
...globalRows.map((row) => ({ ...row, scope: "global" })),
|
|
1601
|
+
...listMcp(profileDirPath).map((row) => ({
|
|
1602
|
+
...row,
|
|
1603
|
+
scope: "profile",
|
|
1604
|
+
...globalIds.has(row.id) ? { shadowed: true } : {}
|
|
1605
|
+
}))
|
|
1606
|
+
],
|
|
1607
|
+
...globalError !== void 0 ? { globalError } : {}
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function mcpScopeDir(scope, profileDirPath, dshHomePath) {
|
|
1611
|
+
return scope === "global" ? dshHomePath : profileDirPath;
|
|
1612
|
+
}
|
|
1539
1613
|
function validateMcpInput(input) {
|
|
1540
1614
|
if (!SERVER_NAME_RE.test(input.serverName)) return "serverName must be 1-32 chars of A-Z a-z 0-9 _ -";
|
|
1541
1615
|
const id = input.id ?? "";
|
|
@@ -1547,9 +1621,9 @@ function validateMcpInput(input) {
|
|
|
1547
1621
|
}
|
|
1548
1622
|
return null;
|
|
1549
1623
|
}
|
|
1550
|
-
function upsertMcp(
|
|
1624
|
+
function upsertMcp(dirPath, input) {
|
|
1551
1625
|
const inputId = input.id ?? "";
|
|
1552
|
-
const doc = loadPatch(
|
|
1626
|
+
const doc = loadPatch(dirPath);
|
|
1553
1627
|
const list = managedInsert(doc);
|
|
1554
1628
|
const existing = inputId !== "" ? mcpRowItems(doc).find(({ node: node2 }) => String(node2.get("id") ?? "") === inputId) : void 0;
|
|
1555
1629
|
let id = inputId !== "" ? inputId : `mcp-${input.serverName}`;
|
|
@@ -1581,21 +1655,21 @@ function upsertMcp(profileDirPath, input) {
|
|
|
1581
1655
|
} else {
|
|
1582
1656
|
rowSeq(doc).items.splice(rowSeq(doc).items.indexOf(existing.node), 1, node);
|
|
1583
1657
|
}
|
|
1584
|
-
savePatch(
|
|
1658
|
+
savePatch(dirPath, doc);
|
|
1585
1659
|
return id;
|
|
1586
1660
|
}
|
|
1587
|
-
function setMcpDisabled(
|
|
1588
|
-
const doc = loadPatch(
|
|
1661
|
+
function setMcpDisabled(dirPath, id, disabled) {
|
|
1662
|
+
const doc = loadPatch(dirPath);
|
|
1589
1663
|
managedInsert(doc);
|
|
1590
1664
|
const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
|
|
1591
1665
|
if (hit === void 0) return false;
|
|
1592
1666
|
if (disabled) hit.node.set("disabled", true);
|
|
1593
1667
|
else hit.node.delete("disabled");
|
|
1594
|
-
savePatch(
|
|
1668
|
+
savePatch(dirPath, doc);
|
|
1595
1669
|
return true;
|
|
1596
1670
|
}
|
|
1597
|
-
function removeMcp(
|
|
1598
|
-
const doc = loadPatch(
|
|
1671
|
+
function removeMcp(dirPath, id) {
|
|
1672
|
+
const doc = loadPatch(dirPath);
|
|
1599
1673
|
managedInsert(doc);
|
|
1600
1674
|
const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
|
|
1601
1675
|
if (hit === void 0 || hit.list === void 0) return false;
|
|
@@ -1605,9 +1679,59 @@ function removeMcp(profileDirPath, id) {
|
|
|
1605
1679
|
if (owner !== void 0 && hit.list.items.length === 0 && owner.items.length === 1) {
|
|
1606
1680
|
seq.items.splice(seq.items.indexOf(owner), 1);
|
|
1607
1681
|
}
|
|
1608
|
-
savePatch(
|
|
1682
|
+
savePatch(dirPath, doc);
|
|
1609
1683
|
return true;
|
|
1610
1684
|
}
|
|
1685
|
+
function mcpRowToInput(row) {
|
|
1686
|
+
return {
|
|
1687
|
+
id: "",
|
|
1688
|
+
serverName: row.serverName,
|
|
1689
|
+
transport: row.transport,
|
|
1690
|
+
disabled: row.disabled,
|
|
1691
|
+
...row.transport === "stdio" ? {
|
|
1692
|
+
command: row.command,
|
|
1693
|
+
...row.args !== void 0 ? { args: row.args } : {},
|
|
1694
|
+
...row.env !== void 0 ? { env: row.env } : {},
|
|
1695
|
+
...row.cwd !== void 0 ? { cwd: row.cwd } : {}
|
|
1696
|
+
} : {
|
|
1697
|
+
url: row.url,
|
|
1698
|
+
...row.headers !== void 0 ? { headers: row.headers } : {}
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function resolveCommandOnPath(command, pathEnv, platform = process.platform) {
|
|
1703
|
+
if (command.includes("/") || command.includes("\\")) return existsSync6(command);
|
|
1704
|
+
const exts = platform === "win32" ? ["", ".com", ".exe", ".bat", ".cmd"] : [""];
|
|
1705
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
1706
|
+
for (const rawDir of pathEnv.split(separator)) {
|
|
1707
|
+
const dir = rawDir.trim().replace(/^"|"$/g, "");
|
|
1708
|
+
if (dir === "") continue;
|
|
1709
|
+
for (const ext of exts) {
|
|
1710
|
+
if (existsSync6(join9(dir, command + ext))) return true;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
async function checkMcpRow(row, options = {}) {
|
|
1716
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
1717
|
+
if (row.transport === "stdio") {
|
|
1718
|
+
const command = row.command ?? "";
|
|
1719
|
+
if (command === "") return { ok: false, detail: "row has no command" };
|
|
1720
|
+
return resolveCommandOnPath(command, pathEnv, options.platform) ? { ok: true, detail: command } : { ok: false, detail: `${command} not found on PATH` };
|
|
1721
|
+
}
|
|
1722
|
+
if (row.url === void 0 || !/^https?:\/\//.test(row.url)) return { ok: false, detail: "row has no http url" };
|
|
1723
|
+
try {
|
|
1724
|
+
const response = await fetch(row.url, {
|
|
1725
|
+
headers: { accept: "application/json, text/event-stream" },
|
|
1726
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
|
|
1727
|
+
});
|
|
1728
|
+
return { ok: true, detail: `HTTP ${response.status}` };
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1731
|
+
const cause = error.cause?.code;
|
|
1732
|
+
return { ok: false, detail: cause !== void 0 ? String(cause) : message };
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1611
1735
|
|
|
1612
1736
|
// src/routes.ts
|
|
1613
1737
|
function customSkillWritable(dir, dshHome) {
|
|
@@ -1633,6 +1757,9 @@ function toSkillRow(skill, dshHome) {
|
|
|
1633
1757
|
function toRootView(entry) {
|
|
1634
1758
|
return { ...entry, live: entry.roots.every((root) => rootExists(root)) };
|
|
1635
1759
|
}
|
|
1760
|
+
function readScope(value) {
|
|
1761
|
+
return value === "global" ? "global" : "profile";
|
|
1762
|
+
}
|
|
1636
1763
|
function mountCapabilitiesRoutes(host, config) {
|
|
1637
1764
|
const disposers = [
|
|
1638
1765
|
host.webServer.register({
|
|
@@ -1949,7 +2076,7 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1949
2076
|
sendJson(response, 502, { error: "market index unavailable (offline?)" });
|
|
1950
2077
|
return;
|
|
1951
2078
|
}
|
|
1952
|
-
const existing = new Set(
|
|
2079
|
+
const existing = new Set(listMcpScoped(config.profileDirPath, config.dshHomePath).servers.map((row) => row.serverName));
|
|
1953
2080
|
sendJson(response, 200, {
|
|
1954
2081
|
source: index.source,
|
|
1955
2082
|
servers: (index.servers ?? []).map((server) => ({ ...server, installed: existing.has(server.id) }))
|
|
@@ -2008,7 +2135,7 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2008
2135
|
sendJson(response, 404, { error: "server not found in the market index" });
|
|
2009
2136
|
return;
|
|
2010
2137
|
}
|
|
2011
|
-
if (
|
|
2138
|
+
if (listMcpScoped(config.profileDirPath, config.dshHomePath).servers.some((row) => row.serverName === server.id)) {
|
|
2012
2139
|
sendJson(response, 409, { error: "already installed" });
|
|
2013
2140
|
return;
|
|
2014
2141
|
}
|
|
@@ -2023,8 +2150,9 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2023
2150
|
sendJson(response, 400, { error: invalid });
|
|
2024
2151
|
return;
|
|
2025
2152
|
}
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2153
|
+
const scope = readScope(body.scope);
|
|
2154
|
+
const id = upsertMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath), input);
|
|
2155
|
+
sendJson(response, 200, { ok: true, id, scope, restartNeeded: true });
|
|
2028
2156
|
} catch (error) {
|
|
2029
2157
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2030
2158
|
}
|
|
@@ -2039,7 +2167,12 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2039
2167
|
response.end();
|
|
2040
2168
|
return;
|
|
2041
2169
|
}
|
|
2042
|
-
|
|
2170
|
+
const listing = listMcpScoped(config.profileDirPath, config.dshHomePath);
|
|
2171
|
+
sendJson(response, 200, {
|
|
2172
|
+
servers: listing.servers,
|
|
2173
|
+
...listing.globalError !== void 0 ? { globalError: listing.globalError } : {},
|
|
2174
|
+
restartNeeded: true
|
|
2175
|
+
});
|
|
2043
2176
|
}
|
|
2044
2177
|
}),
|
|
2045
2178
|
host.webServer.register({
|
|
@@ -2062,8 +2195,9 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2062
2195
|
sendJson(response, 400, { error: invalid });
|
|
2063
2196
|
return;
|
|
2064
2197
|
}
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2198
|
+
const scope = readScope(input.scope);
|
|
2199
|
+
const id = upsertMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath), input);
|
|
2200
|
+
sendJson(response, 200, { ok: true, id, scope, restartNeeded: true });
|
|
2067
2201
|
} catch (error) {
|
|
2068
2202
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2069
2203
|
}
|
|
@@ -2088,7 +2222,8 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2088
2222
|
sendJson(response, 400, { error: "id and disabled are required" });
|
|
2089
2223
|
return;
|
|
2090
2224
|
}
|
|
2091
|
-
const
|
|
2225
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath);
|
|
2226
|
+
const ok = setMcpDisabled(dir, body.id, body.disabled);
|
|
2092
2227
|
sendJson(response, ok ? 200 : 404, ok ? { ok: true, restartNeeded: true } : { error: "server row not found" });
|
|
2093
2228
|
} catch (error) {
|
|
2094
2229
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2114,13 +2249,84 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2114
2249
|
sendJson(response, 400, { error: "id is required" });
|
|
2115
2250
|
return;
|
|
2116
2251
|
}
|
|
2117
|
-
const
|
|
2252
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath);
|
|
2253
|
+
const ok = removeMcp(dir, body.id);
|
|
2118
2254
|
sendJson(response, ok ? 200 : 404, ok ? { ok: true, restartNeeded: true } : { error: "server row not found" });
|
|
2119
2255
|
} catch (error) {
|
|
2120
2256
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2121
2257
|
}
|
|
2122
2258
|
}
|
|
2123
2259
|
}),
|
|
2260
|
+
host.webServer.register({
|
|
2261
|
+
kind: "exact",
|
|
2262
|
+
path: "/dsh-plugin-capabilities/mcp/check",
|
|
2263
|
+
handler: async (request, response) => {
|
|
2264
|
+
if (request.method !== "POST") {
|
|
2265
|
+
response.writeHead(405, { allow: "POST" });
|
|
2266
|
+
response.end();
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (!sameOrigin(request)) {
|
|
2270
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
const body = await readJsonBody(request);
|
|
2275
|
+
if (typeof body.id !== "string") {
|
|
2276
|
+
sendJson(response, 400, { error: "id is required" });
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath);
|
|
2280
|
+
const row = listMcp(dir).find((item) => item.id === body.id);
|
|
2281
|
+
if (row === void 0) {
|
|
2282
|
+
sendJson(response, 404, { error: "server row not found" });
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
sendJson(response, 200, await checkMcpRow(row));
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}),
|
|
2291
|
+
host.webServer.register({
|
|
2292
|
+
kind: "exact",
|
|
2293
|
+
path: "/dsh-plugin-capabilities/mcp/copy",
|
|
2294
|
+
handler: async (request, response) => {
|
|
2295
|
+
if (request.method !== "POST") {
|
|
2296
|
+
response.writeHead(405, { allow: "POST" });
|
|
2297
|
+
response.end();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!sameOrigin(request)) {
|
|
2301
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
try {
|
|
2305
|
+
const body = await readJsonBody(request);
|
|
2306
|
+
if (typeof body.id !== "string") {
|
|
2307
|
+
sendJson(response, 400, { error: "id is required" });
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const scope = readScope(body.scope);
|
|
2311
|
+
const toScope = readScope(body.toScope);
|
|
2312
|
+
const sourceRow = listMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath)).find((item) => item.id === body.id);
|
|
2313
|
+
if (sourceRow === void 0) {
|
|
2314
|
+
sendJson(response, 404, { error: "server row not found" });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const input = mcpRowToInput(sourceRow);
|
|
2318
|
+
const invalid = validateMcpInput(input);
|
|
2319
|
+
if (invalid !== null) {
|
|
2320
|
+
sendJson(response, 400, { error: invalid });
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const id = upsertMcp(mcpScopeDir(toScope, config.profileDirPath, config.dshHomePath), input);
|
|
2324
|
+
sendJson(response, 200, { ok: true, id, scope: toScope, restartNeeded: true });
|
|
2325
|
+
} catch (error) {
|
|
2326
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}),
|
|
2124
2330
|
host.webServer.register({
|
|
2125
2331
|
kind: "exact",
|
|
2126
2332
|
path: "/dsh-plugin-capabilities/import/scan",
|
|
@@ -2133,8 +2339,9 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2133
2339
|
try {
|
|
2134
2340
|
sendJson(response, 200, {
|
|
2135
2341
|
servers: scanAllMcp(),
|
|
2136
|
-
//
|
|
2137
|
-
|
|
2342
|
+
// ServerNames present in either patch layer, so the browser can
|
|
2343
|
+
// grey out existing ones.
|
|
2344
|
+
existing: listMcpScoped(config.profileDirPath, config.dshHomePath).servers.map((row) => row.serverName)
|
|
2138
2345
|
});
|
|
2139
2346
|
} catch (error) {
|
|
2140
2347
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2162,9 +2369,8 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2162
2369
|
const results = [];
|
|
2163
2370
|
for (const server of scanAllMcp()) {
|
|
2164
2371
|
if (!wanted.has(`${server.agent}/${server.name}`)) continue;
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
results.push({ name: server.name, ok: false, error: "already in profile" });
|
|
2372
|
+
if (listMcpScoped(config.profileDirPath, config.dshHomePath).servers.some((row) => row.serverName === server.name)) {
|
|
2373
|
+
results.push({ name: server.name, ok: false, error: "already configured" });
|
|
2168
2374
|
continue;
|
|
2169
2375
|
}
|
|
2170
2376
|
const input = {
|
|
@@ -2178,7 +2384,7 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2178
2384
|
results.push({ name: server.name, ok: false, error: invalid });
|
|
2179
2385
|
continue;
|
|
2180
2386
|
}
|
|
2181
|
-
upsertMcp(config.profileDirPath, input);
|
|
2387
|
+
upsertMcp(mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath), input);
|
|
2182
2388
|
results.push({ name: server.name, ok: true });
|
|
2183
2389
|
}
|
|
2184
2390
|
sendJson(response, 200, { ok: results.every((item) => item.ok), results, restartNeeded: true });
|
|
@@ -2261,6 +2467,7 @@ function apply(ctx, config) {
|
|
|
2261
2467
|
ctx.effect(
|
|
2262
2468
|
() => mountCapabilitiesRoutes(hostCtx, {
|
|
2263
2469
|
profileDirPath: profileDir(profile),
|
|
2470
|
+
dshHomePath: dshHomeDir(),
|
|
2264
2471
|
remountProvider
|
|
2265
2472
|
}),
|
|
2266
2473
|
"dsh-plugin-capabilities: http routes"
|