dsh-plugin-capabilities 0.3.8 → 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 +3 -3
- package/lib/client.js +96 -13
- package/lib/client.js.map +2 -2
- package/lib/index.js +175 -8
- 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 +69 -4
- package/src/client/index.ts +7 -4
- package/src/client/locales.ts +14 -4
- package/src/mcp.test.ts +70 -2
- package/src/mcp.ts +78 -0
- package/src/routes.ts +81 -6
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);
|
|
@@ -1636,6 +1682,56 @@ function removeMcp(dirPath, id) {
|
|
|
1636
1682
|
savePatch(dirPath, doc);
|
|
1637
1683
|
return true;
|
|
1638
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
|
+
}
|
|
1639
1735
|
|
|
1640
1736
|
// src/routes.ts
|
|
1641
1737
|
function customSkillWritable(dir, dshHome) {
|
|
@@ -2054,8 +2150,9 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2054
2150
|
sendJson(response, 400, { error: invalid });
|
|
2055
2151
|
return;
|
|
2056
2152
|
}
|
|
2057
|
-
const
|
|
2058
|
-
|
|
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 });
|
|
2059
2156
|
} catch (error) {
|
|
2060
2157
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2061
2158
|
}
|
|
@@ -2160,6 +2257,76 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2160
2257
|
}
|
|
2161
2258
|
}
|
|
2162
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
|
+
}),
|
|
2163
2330
|
host.webServer.register({
|
|
2164
2331
|
kind: "exact",
|
|
2165
2332
|
path: "/dsh-plugin-capabilities/import/scan",
|
|
@@ -2217,7 +2384,7 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
2217
2384
|
results.push({ name: server.name, ok: false, error: invalid });
|
|
2218
2385
|
continue;
|
|
2219
2386
|
}
|
|
2220
|
-
upsertMcp(config.profileDirPath, input);
|
|
2387
|
+
upsertMcp(mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath), input);
|
|
2221
2388
|
results.push({ name: server.name, ok: true });
|
|
2222
2389
|
}
|
|
2223
2390
|
sendJson(response, 200, { ok: results.every((item) => item.ok), results, restartNeeded: true });
|