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/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 mapClaudeEntry(name2, entry) {
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: "claude-code",
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: "claude-code",
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 = mapClaudeEntry(name2, entry);
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 id = upsertMcp(config.profileDirPath, input);
2058
- sendJson(response, 200, { ok: true, id, restartNeeded: true });
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 });