multicorn-shield 1.6.0 → 1.7.0

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/CHANGELOG.md CHANGED
@@ -9,7 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - Bump `version` in `package.json` before publishing to npm.
11
11
 
12
- ## [1.5.0] - 2026-05-10
12
+ ## [1.7.0] - 2026-05-11
13
+
14
+ ### Fixed
15
+
16
+ - Agent name now uses user-provided short name instead of auto-generated default
17
+ - Goose config snippet uses `uri` instead of incorrect `url`
18
+ - Replace flow deduplicates agent list loaded from config on startup
19
+
20
+ ### Changed
21
+
22
+ - Add runtime dependency on [`yaml`](https://www.npmjs.com/package/yaml) (ISC) for safe Goose `config.yaml` read/write in the hosted-proxy CLI path
23
+ - GitHub Copilot: CLI auto-writes config to `.vscode/mcp.json`
24
+ - Kilo Code: CLI writes to `.kilo/kilo.jsonc` with correct format (`mcp` key, `type: remote`)
25
+ - Continue: CLI writes YAML to `.continue/mcpServers/<name>.yaml` in workspace root
26
+ - Goose: CLI auto-writes to `~/.config/goose/config.yaml`
27
+ - All hosted proxy platforms: Next Steps includes where to verify connection and example prompt
28
+ - Claude Desktop removed from hosted proxy platform list (consent URL not clickable)
29
+
30
+ ## [1.6.0] - 2026-05-10
13
31
 
14
32
  ### Added
15
33
 
@@ -8,6 +8,7 @@ import { readFileSync, existsSync, statSync } from 'fs';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { createRequire } from 'module';
10
10
  import { createInterface } from 'readline';
11
+ import { parse, stringify } from 'yaml';
11
12
  import 'stream';
12
13
 
13
14
  var __defProp = Object.defineProperty;
@@ -1346,9 +1347,6 @@ function getClineMcpSettingsPath() {
1346
1347
  );
1347
1348
  }
1348
1349
  }
1349
- function getContinueConfigJsonPath() {
1350
- return join(homedir(), ".continue", "config.json");
1351
- }
1352
1350
  function platformMenuLabelForSelection(sel) {
1353
1351
  const slug = PLATFORM_BY_SELECTION[sel];
1354
1352
  if (slug === void 0) return "Unknown";
@@ -1650,13 +1648,56 @@ function writeMcpAddedLine(shortName, filePath) {
1650
1648
  style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
1651
1649
  );
1652
1650
  }
1653
- async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1651
+ function sanitiseYamlValue(value) {
1652
+ if (value.length === 0) {
1653
+ return "''";
1654
+ }
1655
+ const needsQuoting = /[:#\n{}]/.test(value) || value.includes("[") || value.includes("]") || value !== value.trim() || value.includes("'");
1656
+ if (!needsQuoting) {
1657
+ return value;
1658
+ }
1659
+ return `'${value.replace(/'/g, "''")}'`;
1660
+ }
1661
+ function gitignoreLikelyCoversPath(relPosixPath, gitignoreBody) {
1662
+ const norm = relPosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1663
+ const lines = gitignoreBody.split(/\r?\n/);
1664
+ for (const raw of lines) {
1665
+ const line = raw.trim();
1666
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
1667
+ const pat = line.replace(/^\//, "");
1668
+ if (pat === norm || pat === `./${norm}`) return true;
1669
+ if (!pat.includes("*")) {
1670
+ if (pat.endsWith("/")) {
1671
+ const dir = pat.slice(0, -1);
1672
+ if (norm === dir || norm.startsWith(`${dir}/`)) return true;
1673
+ }
1674
+ }
1675
+ }
1676
+ return false;
1677
+ }
1678
+ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
1679
+ const gitignorePath = join(workspaceRoot, ".gitignore");
1680
+ let content;
1681
+ try {
1682
+ content = await readFile(gitignorePath, "utf8");
1683
+ } catch (e) {
1684
+ if (isErrnoException(e) && e.code === "ENOENT") return;
1685
+ throw e;
1686
+ }
1687
+ const norm = relativePosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1688
+ if (gitignoreLikelyCoversPath(norm, content)) return;
1689
+ process.stderr.write(
1690
+ style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
1691
+ );
1692
+ }
1693
+ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
1654
1694
  let root = {};
1655
1695
  try {
1656
1696
  const raw = await readFile(filePath, "utf8");
1697
+ const toParse = options.stripJsonComments ? raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "") : raw;
1657
1698
  let parsed;
1658
1699
  try {
1659
- parsed = JSON.parse(raw);
1700
+ parsed = JSON.parse(toParse);
1660
1701
  } catch {
1661
1702
  return "parse-error";
1662
1703
  }
@@ -1672,15 +1713,25 @@ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1672
1713
  throw e;
1673
1714
  }
1674
1715
  }
1675
- const mcpRaw = root["mcpServers"];
1676
- const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
1677
- mcpServers[shortName] = entry;
1678
- root["mcpServers"] = mcpServers;
1716
+ const bucketRaw = root[topLevelKey];
1717
+ const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
1718
+ if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
1719
+ return "unchanged";
1720
+ }
1721
+ bucket[shortName] = entry;
1722
+ root[topLevelKey] = bucket;
1679
1723
  await mkdir(dirname(filePath), { recursive: true });
1680
1724
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1681
1725
  writeMcpAddedLine(shortName, filePath);
1682
1726
  return "ok";
1683
1727
  }
1728
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1729
+ const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
1730
+ stripJsonComments: false,
1731
+ onExisting: "overwrite"
1732
+ });
1733
+ return result === "parse-error" ? "parse-error" : "ok";
1734
+ }
1684
1735
  async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1685
1736
  const entry = {
1686
1737
  command: "npx",
@@ -1688,8 +1739,30 @@ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1688
1739
  };
1689
1740
  return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
1690
1741
  }
1691
- async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1692
- const filePath = getContinueConfigJsonPath();
1742
+ async function mergeContinueHostedMcp(workspacePath, shortName, proxyUrl, apiKey) {
1743
+ const dir = join(workspacePath, ".continue", "mcpServers");
1744
+ const filePath = join(dir, `${shortName}.yaml`);
1745
+ const sn = sanitiseYamlValue(shortName);
1746
+ const urlEsc = sanitiseYamlValue(proxyUrl);
1747
+ const authEsc = sanitiseYamlValue(`Bearer ${apiKey}`);
1748
+ const yaml = `name: ${sn}
1749
+ version: 0.0.1
1750
+ schema: v1
1751
+ mcpServers:
1752
+ - name: ${sn}
1753
+ type: streamable-http
1754
+ url: ${urlEsc}
1755
+ headers:
1756
+ Authorization: ${authEsc}
1757
+ `;
1758
+ await mkdir(dir, { recursive: true });
1759
+ await writeFile(filePath, yaml, SECRET_JSON_FILE_OPTIONS);
1760
+ writeMcpAddedLine(shortName, filePath);
1761
+ await warnIfApiKeyFileNotGitignored(workspacePath, `.continue/mcpServers/${shortName}.yaml`);
1762
+ return "ok";
1763
+ }
1764
+ async function mergeCopilotVscodeMcp(workspacePath, shortName, proxyUrl, apiKey) {
1765
+ const filePath = join(workspacePath, ".vscode", "mcp.json");
1693
1766
  let root = {};
1694
1767
  try {
1695
1768
  const raw = await readFile(filePath, "utf8");
@@ -1711,43 +1784,49 @@ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1711
1784
  throw e;
1712
1785
  }
1713
1786
  }
1714
- const rawServers = root["mcpServers"];
1715
- if (rawServers !== void 0) {
1716
- if (Array.isArray(rawServers)) ; else if (typeof rawServers === "object" && rawServers !== null) {
1717
- return "parse-error";
1718
- } else {
1719
- return "parse-error";
1720
- }
1721
- }
1722
- const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
1723
- const entry = {
1724
- name: shortName,
1725
- type: "streamable-http",
1787
+ const serversRaw = root["servers"];
1788
+ const servers = typeof serversRaw === "object" && serversRaw !== null && !Array.isArray(serversRaw) ? { ...serversRaw } : {};
1789
+ const existed = servers[shortName] !== void 0;
1790
+ servers[shortName] = {
1791
+ type: "http",
1726
1792
  url: proxyUrl,
1727
- headers: {
1728
- Authorization: `Bearer ${apiKey}`
1729
- }
1793
+ headers: { Authorization: `Bearer ${apiKey}` }
1730
1794
  };
1731
- const idx = servers.findIndex((s) => s["name"] === shortName);
1732
- if (idx >= 0) {
1733
- servers[idx] = entry;
1734
- } else {
1735
- servers.push(entry);
1736
- }
1737
- root["mcpServers"] = servers;
1795
+ root["servers"] = servers;
1738
1796
  await mkdir(dirname(filePath), { recursive: true });
1739
1797
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1740
- writeMcpAddedLine(shortName, filePath);
1798
+ if (existed) {
1799
+ process.stderr.write(
1800
+ style.dim(`Updated existing server entry for ${shortName} in .vscode/mcp.json`) + "\n"
1801
+ );
1802
+ } else {
1803
+ writeMcpAddedLine(shortName, filePath);
1804
+ }
1805
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".vscode/mcp.json");
1741
1806
  return "ok";
1742
1807
  }
1743
1808
  async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1744
- const filePath = join(workspacePath, ".kilocode", "mcp.json");
1745
- return mergeMcpServersObjectStyle(filePath, shortName, {
1746
- url: proxyUrl,
1747
- headers: {
1748
- Authorization: `Bearer ${apiKey}`
1809
+ const filePath = join(workspacePath, ".kilo", "kilo.jsonc");
1810
+ const result = await mergeTopLevelKeyedJsonFile(
1811
+ filePath,
1812
+ "mcp",
1813
+ shortName,
1814
+ {
1815
+ type: "remote",
1816
+ url: proxyUrl,
1817
+ headers: { Authorization: `Bearer ${apiKey}` },
1818
+ enabled: true
1819
+ },
1820
+ {
1821
+ stripJsonComments: true,
1822
+ onExisting: "skip"
1749
1823
  }
1750
- });
1824
+ );
1825
+ if (result === "parse-error") return "parse-error";
1826
+ if (result === "ok") {
1827
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".kilo/kilo.jsonc");
1828
+ }
1829
+ return "ok";
1751
1830
  }
1752
1831
  function printHostedProxyJsonParseWarning(filePath) {
1753
1832
  process.stderr.write(
@@ -1788,7 +1867,7 @@ function printHostedProxyPostWriteHints(platform, shortName) {
1788
1867
  }
1789
1868
  if (platform === "kilo-code") {
1790
1869
  process.stderr.write(
1791
- style.dim("Restart Kilo Code or reload the window so it picks up .kilocode/mcp.json.") + "\n"
1870
+ style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
1792
1871
  );
1793
1872
  }
1794
1873
  }
@@ -1805,15 +1884,40 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1805
1884
  return;
1806
1885
  }
1807
1886
  if (platform === "github-copilot") {
1808
- process.stderr.write(
1809
- "\n" + style.dim(
1810
- "GitHub Copilot uses VS Code settings - paste the snippet below into your VS Code Settings (JSON)."
1811
- ) + "\n"
1812
- );
1887
+ try {
1888
+ const result = await mergeCopilotVscodeMcp(
1889
+ workspacePath,
1890
+ shortName,
1891
+ proxyUrlWithKeyWhenNeeded,
1892
+ apiKey
1893
+ );
1894
+ if (result === "ok") {
1895
+ printHostedProxyPostWriteHints(platform, shortName);
1896
+ return;
1897
+ }
1898
+ printHostedProxyJsonParseWarning(join(workspacePath, ".vscode", "mcp.json"));
1899
+ } catch (err) {
1900
+ process.stderr.write(
1901
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
1902
+ `
1903
+ );
1904
+ }
1813
1905
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1814
1906
  return;
1815
1907
  }
1816
1908
  if (platform === "goose") {
1909
+ try {
1910
+ const result = await mergeGooseConfig(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
1911
+ if (result === "ok") {
1912
+ printHostedProxyPostWriteHints(platform, shortName);
1913
+ return;
1914
+ }
1915
+ } catch (err) {
1916
+ process.stderr.write(
1917
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
1918
+ `
1919
+ );
1920
+ }
1817
1921
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1818
1922
  return;
1819
1923
  }
@@ -1860,12 +1964,19 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1860
1964
  apiKey
1861
1965
  );
1862
1966
  if (result === "parse-error") {
1863
- printHostedProxyJsonParseWarning(join(workspacePath, ".kilocode", "mcp.json"));
1967
+ printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
1864
1968
  }
1865
1969
  } else if (platform === "continue-dev") {
1866
- result = await mergeContinueHostedMcp(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
1970
+ result = await mergeContinueHostedMcp(
1971
+ workspacePath,
1972
+ shortName,
1973
+ proxyUrlWithKeyWhenNeeded,
1974
+ apiKey
1975
+ );
1867
1976
  if (result === "parse-error") {
1868
- printHostedProxyJsonParseWarning(getContinueConfigJsonPath());
1977
+ printHostedProxyJsonParseWarning(
1978
+ join(workspacePath, ".continue", "mcpServers", `${shortName}.yaml`)
1979
+ );
1869
1980
  }
1870
1981
  } else {
1871
1982
  result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
@@ -1889,16 +2000,95 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1889
2000
  }
1890
2001
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1891
2002
  }
1892
- function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
1893
- return `extensions:
1894
- ${shortName}:
2003
+ function printGooseConfigYamlParseErrorToStderr() {
2004
+ process.stderr.write(
2005
+ style.yellow("!") + " Could not parse ~/.config/goose/config.yaml - check for syntax errors or invalid YAML\n"
2006
+ );
2007
+ }
2008
+ function gooseExtensionYaml(shortName, proxyUrl, bearerHeader) {
2009
+ const sn = sanitiseYamlValue(shortName);
2010
+ const urlEsc = sanitiseYamlValue(proxyUrl);
2011
+ const authEsc = sanitiseYamlValue(bearerHeader);
2012
+ return ` ${sn}:
2013
+ enabled: true
1895
2014
  type: streamable_http
1896
- url: ${proxyUrl}
2015
+ name: ${sn}
2016
+ description: ''
2017
+ uri: ${urlEsc}
2018
+ envs: {}
2019
+ env_keys: []
1897
2020
  headers:
1898
- Authorization: ${bearerHeader}
1899
- enabled: true
2021
+ Authorization: ${authEsc}
1900
2022
  timeout: 300
2023
+ socket: null
2024
+ bundled: null
2025
+ available_tools: []
2026
+ `;
2027
+ }
2028
+ function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
2029
+ return `extensions:
2030
+ ` + gooseExtensionYaml(shortName, proxyUrl, bearerHeader);
2031
+ }
2032
+ function isYamlPlainObject(value) {
2033
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2034
+ }
2035
+ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2036
+ const filePath = join(homedir(), ".config", "goose", "config.yaml");
2037
+ const bearerHeader = `Bearer ${apiKey}`;
2038
+ let content = "";
2039
+ try {
2040
+ content = await readFile(filePath, "utf8");
2041
+ } catch (e) {
2042
+ if (isErrnoException(e) && e.code === "ENOENT") {
2043
+ content = "";
2044
+ } else {
2045
+ throw e;
2046
+ }
2047
+ }
2048
+ let root;
2049
+ try {
2050
+ const data = content.trim().length === 0 ? {} : parse(content);
2051
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
2052
+ printGooseConfigYamlParseErrorToStderr();
2053
+ return "parse-error";
2054
+ }
2055
+ root = data;
2056
+ } catch {
2057
+ printGooseConfigYamlParseErrorToStderr();
2058
+ return "parse-error";
2059
+ }
2060
+ const extensionsRaw = root["extensions"];
2061
+ let extensions;
2062
+ if (isYamlPlainObject(extensionsRaw)) {
2063
+ extensions = { ...extensionsRaw };
2064
+ } else if (extensionsRaw === void 0) {
2065
+ extensions = {};
2066
+ } else {
2067
+ printGooseConfigYamlParseErrorToStderr();
2068
+ return "parse-error";
2069
+ }
2070
+ extensions[shortName] = {
2071
+ enabled: true,
2072
+ type: "streamable_http",
2073
+ name: shortName,
2074
+ description: "",
2075
+ uri: proxyUrl,
2076
+ envs: {},
2077
+ env_keys: [],
2078
+ headers: { Authorization: bearerHeader },
2079
+ timeout: 300,
2080
+ socket: null,
2081
+ bundled: null,
2082
+ available_tools: []
2083
+ };
2084
+ root["extensions"] = extensions;
2085
+ const out = stringify(root, { indent: 2, lineWidth: 0 });
2086
+ const body = out.endsWith("\n") ? out : `${out}
1901
2087
  `;
2088
+ await mkdir(dirname(filePath), { recursive: true });
2089
+ await writeFile(filePath, body, SECRET_JSON_FILE_OPTIONS);
2090
+ writeMcpAddedLine(shortName, filePath);
2091
+ return "ok";
1902
2092
  }
1903
2093
  function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1904
2094
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
@@ -1919,14 +2109,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1919
2109
  if (platform === "github-copilot") {
1920
2110
  snippetText = JSON.stringify(
1921
2111
  {
1922
- mcp: {
1923
- servers: {
1924
- [shortName]: {
1925
- type: "http",
1926
- url: urlInSnippet,
1927
- headers: {
1928
- Authorization: authHeader
1929
- }
2112
+ servers: {
2113
+ [shortName]: {
2114
+ type: "http",
2115
+ url: urlInSnippet,
2116
+ headers: {
2117
+ Authorization: authHeader
1930
2118
  }
1931
2119
  }
1932
2120
  }
@@ -1965,18 +2153,32 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1965
2153
  2
1966
2154
  );
1967
2155
  } else if (platform === "continue-dev") {
2156
+ const sn = sanitiseYamlValue(shortName);
2157
+ const urlEsc = sanitiseYamlValue(urlInSnippet);
2158
+ const authEsc = sanitiseYamlValue(authHeader);
2159
+ snippetText = `name: ${sn}
2160
+ version: 0.0.1
2161
+ schema: v1
2162
+ mcpServers:
2163
+ - name: ${sn}
2164
+ type: streamable-http
2165
+ url: ${urlEsc}
2166
+ headers:
2167
+ Authorization: ${authEsc}
2168
+ `;
2169
+ } else if (platform === "kilo-code") {
1968
2170
  snippetText = JSON.stringify(
1969
2171
  {
1970
- mcpServers: [
1971
- {
1972
- name: shortName,
1973
- type: "streamable-http",
2172
+ mcp: {
2173
+ [shortName]: {
2174
+ type: "remote",
1974
2175
  url: urlInSnippet,
1975
2176
  headers: {
1976
2177
  Authorization: authHeader
1977
- }
2178
+ },
2179
+ enabled: true
1978
2180
  }
1979
- ]
2181
+ }
1980
2182
  },
1981
2183
  null,
1982
2184
  2
@@ -2016,16 +2218,18 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2016
2218
  );
2017
2219
  } else if (platform === "kilo-code") {
2018
2220
  process.stderr.write(
2019
- "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilocode", "mcp.json")}:`) + "\n\n"
2221
+ "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
2020
2222
  );
2021
2223
  } else if (platform === "github-copilot") {
2022
2224
  process.stderr.write(
2023
2225
  "\n" + style.dim(
2024
- "Merge this snippet under the mcp key in your VS Code Settings (JSON). If you do not have an mcp section yet, add one. Copilot picks up MCP servers when you use Agent mode."
2226
+ "Create .vscode/mcp.json in your workspace root (create the .vscode folder if it does not exist). After saving, reload VS Code and confirm the server appears in Copilot Agent mode under Tools."
2025
2227
  ) + "\n\n"
2026
2228
  );
2027
2229
  } else if (platform === "continue-dev") {
2028
- process.stderr.write("\n" + style.dim(`Add this to ${getContinueConfigJsonPath()}:`) + "\n\n");
2230
+ process.stderr.write(
2231
+ "\n" + style.dim(`Save this as .continue/mcpServers/${shortName}.yaml in your workspace root.`) + "\n\n"
2232
+ );
2029
2233
  } else if (platform === "goose") {
2030
2234
  process.stderr.write(
2031
2235
  "\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
@@ -2201,7 +2405,7 @@ async function runInit(explicitBaseUrl, options) {
2201
2405
  }
2202
2406
  await warnIfInstalledShieldIsOutdated();
2203
2407
  const configuredAgents = [];
2204
- let currentAgents = collectAgentsFromConfig(existing);
2408
+ let currentAgents = mergeAgentsForUniqueNames(collectAgentsFromConfig(existing));
2205
2409
  let lastConfig = {
2206
2410
  apiKey,
2207
2411
  baseUrl: resolvedBaseUrl,
@@ -2862,23 +3066,27 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2862
3066
  );
2863
3067
  }
2864
3068
  if (configuredPlatforms.has("kilo-code")) {
3069
+ const kiloLabel = mcpPromptLabel2("kilo-code");
2865
3070
  blocks.push(
2866
- "\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Try it: make a request in Kilo Code - Shield will intercept the first tool call and ask for your consent\n"
3071
+ "\n" + style.bold("Kilo Code") + '\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Confirm connection: Settings \u2192 Agent Behaviour \u2192 MCP Servers\n \u2192 Try it: paste this into Kilo Code:\n "Use the ' + kiloLabel + ' MCP server to list my GitHub repositories"\n'
2867
3072
  );
2868
3073
  }
2869
3074
  if (configuredPlatforms.has("github-copilot")) {
3075
+ const copilotLabel = mcpPromptLabel2("github-copilot");
2870
3076
  blocks.push(
2871
- "\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Open Copilot Agent mode and confirm the MCP server connects\n \u2192 Try it: make a request in GitHub Copilot - Shield will intercept the first tool call and ask for your consent\n"
3077
+ "\n" + style.bold("GitHub Copilot") + '\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Confirm connection: open Copilot chat in Agent mode and confirm the server appears under Tools\n \u2192 Try it: paste this into GitHub Copilot:\n "Use the ' + copilotLabel + ' MCP server to list my GitHub repositories"\n'
2872
3078
  );
2873
3079
  }
2874
3080
  if (configuredPlatforms.has("continue-dev")) {
3081
+ const continueLabel = mcpPromptLabel2("continue-dev");
2875
3082
  blocks.push(
2876
- "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Try it: make a request in Continue - Shield will intercept the first tool call and ask for your consent\n"
3083
+ "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + '\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Confirm connection: Settings \u2192 Tools \u2192 MCP Servers\n \u2192 Try it: paste this into Continue:\n "Use the ' + continueLabel + ' MCP server to list my GitHub repositories"\n'
2877
3084
  );
2878
3085
  }
2879
3086
  if (configuredPlatforms.has("goose")) {
3087
+ const gooseLabel = mcpPromptLabel2("goose");
2880
3088
  blocks.push(
2881
- "\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n \u2192 Try it: make a request in Goose - Shield will intercept the first tool call and ask for your consent\n"
3089
+ "\n" + style.bold("Goose") + '\n \u2192 Start a new Goose session after updating config\n \u2192 Confirm connection: check the Extensions page in the sidebar\n \u2192 Try it: paste this into Goose:\n "Use the ' + gooseLabel + ' MCP server to list my GitHub repositories"\n'
2882
3090
  );
2883
3091
  }
2884
3092
  const windsurfNativeConfigured = configuredAgents.some(
@@ -3992,7 +4200,7 @@ var init_package = __esm({
3992
4200
  "package.json"() {
3993
4201
  package_default = {
3994
4202
  name: "multicorn-shield",
3995
- version: "1.6.0",
4203
+ version: "1.7.0",
3996
4204
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
3997
4205
  license: "MIT",
3998
4206
  author: "Multicorn AI Pty Ltd",
@@ -4083,6 +4291,7 @@ var init_package = __esm({
4083
4291
  dependencies: {
4084
4292
  "@modelcontextprotocol/sdk": "^1.27.1",
4085
4293
  lit: "^3.2.0",
4294
+ yaml: "^2.8.2",
4086
4295
  zod: "^4.3.6"
4087
4296
  },
4088
4297
  devDependencies: {
@@ -6,6 +6,7 @@ import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { createRequire } from 'module';
8
8
  import { createInterface } from 'readline';
9
+ import { parse, stringify } from 'yaml';
9
10
  import { spawn } from 'child_process';
10
11
  import { createHash } from 'crypto';
11
12
  import 'stream';
@@ -1360,9 +1361,6 @@ function getClineMcpSettingsPath() {
1360
1361
  );
1361
1362
  }
1362
1363
  }
1363
- function getContinueConfigJsonPath() {
1364
- return join(homedir(), ".continue", "config.json");
1365
- }
1366
1364
  var INIT_WIZARD_PLATFORM_REGISTRY = [
1367
1365
  { slug: "openclaw", displayName: "OpenClaw", section: "native" },
1368
1366
  { slug: "claude-code", displayName: "Claude Code", section: "native" },
@@ -1730,13 +1728,56 @@ function writeMcpAddedLine(shortName, filePath) {
1730
1728
  style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
1731
1729
  );
1732
1730
  }
1733
- async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1731
+ function sanitiseYamlValue(value) {
1732
+ if (value.length === 0) {
1733
+ return "''";
1734
+ }
1735
+ const needsQuoting = /[:#\n{}]/.test(value) || value.includes("[") || value.includes("]") || value !== value.trim() || value.includes("'");
1736
+ if (!needsQuoting) {
1737
+ return value;
1738
+ }
1739
+ return `'${value.replace(/'/g, "''")}'`;
1740
+ }
1741
+ function gitignoreLikelyCoversPath(relPosixPath, gitignoreBody) {
1742
+ const norm = relPosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1743
+ const lines = gitignoreBody.split(/\r?\n/);
1744
+ for (const raw of lines) {
1745
+ const line = raw.trim();
1746
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
1747
+ const pat = line.replace(/^\//, "");
1748
+ if (pat === norm || pat === `./${norm}`) return true;
1749
+ if (!pat.includes("*")) {
1750
+ if (pat.endsWith("/")) {
1751
+ const dir = pat.slice(0, -1);
1752
+ if (norm === dir || norm.startsWith(`${dir}/`)) return true;
1753
+ }
1754
+ }
1755
+ }
1756
+ return false;
1757
+ }
1758
+ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
1759
+ const gitignorePath = join(workspaceRoot, ".gitignore");
1760
+ let content;
1761
+ try {
1762
+ content = await readFile(gitignorePath, "utf8");
1763
+ } catch (e) {
1764
+ if (isErrnoException(e) && e.code === "ENOENT") return;
1765
+ throw e;
1766
+ }
1767
+ const norm = relativePosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1768
+ if (gitignoreLikelyCoversPath(norm, content)) return;
1769
+ process.stderr.write(
1770
+ style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
1771
+ );
1772
+ }
1773
+ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
1734
1774
  let root = {};
1735
1775
  try {
1736
1776
  const raw = await readFile(filePath, "utf8");
1777
+ const toParse = options.stripJsonComments ? raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "") : raw;
1737
1778
  let parsed;
1738
1779
  try {
1739
- parsed = JSON.parse(raw);
1780
+ parsed = JSON.parse(toParse);
1740
1781
  } catch {
1741
1782
  return "parse-error";
1742
1783
  }
@@ -1752,15 +1793,25 @@ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1752
1793
  throw e;
1753
1794
  }
1754
1795
  }
1755
- const mcpRaw = root["mcpServers"];
1756
- const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
1757
- mcpServers[shortName] = entry;
1758
- root["mcpServers"] = mcpServers;
1796
+ const bucketRaw = root[topLevelKey];
1797
+ const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
1798
+ if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
1799
+ return "unchanged";
1800
+ }
1801
+ bucket[shortName] = entry;
1802
+ root[topLevelKey] = bucket;
1759
1803
  await mkdir(dirname(filePath), { recursive: true });
1760
1804
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1761
1805
  writeMcpAddedLine(shortName, filePath);
1762
1806
  return "ok";
1763
1807
  }
1808
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1809
+ const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
1810
+ stripJsonComments: false,
1811
+ onExisting: "overwrite"
1812
+ });
1813
+ return result === "parse-error" ? "parse-error" : "ok";
1814
+ }
1764
1815
  async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1765
1816
  const entry = {
1766
1817
  command: "npx",
@@ -1768,8 +1819,30 @@ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1768
1819
  };
1769
1820
  return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
1770
1821
  }
1771
- async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1772
- const filePath = getContinueConfigJsonPath();
1822
+ async function mergeContinueHostedMcp(workspacePath, shortName, proxyUrl, apiKey) {
1823
+ const dir = join(workspacePath, ".continue", "mcpServers");
1824
+ const filePath = join(dir, `${shortName}.yaml`);
1825
+ const sn = sanitiseYamlValue(shortName);
1826
+ const urlEsc = sanitiseYamlValue(proxyUrl);
1827
+ const authEsc = sanitiseYamlValue(`Bearer ${apiKey}`);
1828
+ const yaml = `name: ${sn}
1829
+ version: 0.0.1
1830
+ schema: v1
1831
+ mcpServers:
1832
+ - name: ${sn}
1833
+ type: streamable-http
1834
+ url: ${urlEsc}
1835
+ headers:
1836
+ Authorization: ${authEsc}
1837
+ `;
1838
+ await mkdir(dir, { recursive: true });
1839
+ await writeFile(filePath, yaml, SECRET_JSON_FILE_OPTIONS);
1840
+ writeMcpAddedLine(shortName, filePath);
1841
+ await warnIfApiKeyFileNotGitignored(workspacePath, `.continue/mcpServers/${shortName}.yaml`);
1842
+ return "ok";
1843
+ }
1844
+ async function mergeCopilotVscodeMcp(workspacePath, shortName, proxyUrl, apiKey) {
1845
+ const filePath = join(workspacePath, ".vscode", "mcp.json");
1773
1846
  let root = {};
1774
1847
  try {
1775
1848
  const raw = await readFile(filePath, "utf8");
@@ -1791,43 +1864,49 @@ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1791
1864
  throw e;
1792
1865
  }
1793
1866
  }
1794
- const rawServers = root["mcpServers"];
1795
- if (rawServers !== void 0) {
1796
- if (Array.isArray(rawServers)) ; else if (typeof rawServers === "object" && rawServers !== null) {
1797
- return "parse-error";
1798
- } else {
1799
- return "parse-error";
1800
- }
1801
- }
1802
- const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
1803
- const entry = {
1804
- name: shortName,
1805
- type: "streamable-http",
1867
+ const serversRaw = root["servers"];
1868
+ const servers = typeof serversRaw === "object" && serversRaw !== null && !Array.isArray(serversRaw) ? { ...serversRaw } : {};
1869
+ const existed = servers[shortName] !== void 0;
1870
+ servers[shortName] = {
1871
+ type: "http",
1806
1872
  url: proxyUrl,
1807
- headers: {
1808
- Authorization: `Bearer ${apiKey}`
1809
- }
1873
+ headers: { Authorization: `Bearer ${apiKey}` }
1810
1874
  };
1811
- const idx = servers.findIndex((s) => s["name"] === shortName);
1812
- if (idx >= 0) {
1813
- servers[idx] = entry;
1814
- } else {
1815
- servers.push(entry);
1816
- }
1817
- root["mcpServers"] = servers;
1875
+ root["servers"] = servers;
1818
1876
  await mkdir(dirname(filePath), { recursive: true });
1819
1877
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1820
- writeMcpAddedLine(shortName, filePath);
1878
+ if (existed) {
1879
+ process.stderr.write(
1880
+ style.dim(`Updated existing server entry for ${shortName} in .vscode/mcp.json`) + "\n"
1881
+ );
1882
+ } else {
1883
+ writeMcpAddedLine(shortName, filePath);
1884
+ }
1885
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".vscode/mcp.json");
1821
1886
  return "ok";
1822
1887
  }
1823
1888
  async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1824
- const filePath = join(workspacePath, ".kilocode", "mcp.json");
1825
- return mergeMcpServersObjectStyle(filePath, shortName, {
1826
- url: proxyUrl,
1827
- headers: {
1828
- Authorization: `Bearer ${apiKey}`
1889
+ const filePath = join(workspacePath, ".kilo", "kilo.jsonc");
1890
+ const result = await mergeTopLevelKeyedJsonFile(
1891
+ filePath,
1892
+ "mcp",
1893
+ shortName,
1894
+ {
1895
+ type: "remote",
1896
+ url: proxyUrl,
1897
+ headers: { Authorization: `Bearer ${apiKey}` },
1898
+ enabled: true
1899
+ },
1900
+ {
1901
+ stripJsonComments: true,
1902
+ onExisting: "skip"
1829
1903
  }
1830
- });
1904
+ );
1905
+ if (result === "parse-error") return "parse-error";
1906
+ if (result === "ok") {
1907
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".kilo/kilo.jsonc");
1908
+ }
1909
+ return "ok";
1831
1910
  }
1832
1911
  function printHostedProxyJsonParseWarning(filePath) {
1833
1912
  process.stderr.write(
@@ -1868,7 +1947,7 @@ function printHostedProxyPostWriteHints(platform, shortName) {
1868
1947
  }
1869
1948
  if (platform === "kilo-code") {
1870
1949
  process.stderr.write(
1871
- style.dim("Restart Kilo Code or reload the window so it picks up .kilocode/mcp.json.") + "\n"
1950
+ style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
1872
1951
  );
1873
1952
  }
1874
1953
  }
@@ -1885,15 +1964,40 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1885
1964
  return;
1886
1965
  }
1887
1966
  if (platform === "github-copilot") {
1888
- process.stderr.write(
1889
- "\n" + style.dim(
1890
- "GitHub Copilot uses VS Code settings - paste the snippet below into your VS Code Settings (JSON)."
1891
- ) + "\n"
1892
- );
1967
+ try {
1968
+ const result = await mergeCopilotVscodeMcp(
1969
+ workspacePath,
1970
+ shortName,
1971
+ proxyUrlWithKeyWhenNeeded,
1972
+ apiKey
1973
+ );
1974
+ if (result === "ok") {
1975
+ printHostedProxyPostWriteHints(platform, shortName);
1976
+ return;
1977
+ }
1978
+ printHostedProxyJsonParseWarning(join(workspacePath, ".vscode", "mcp.json"));
1979
+ } catch (err) {
1980
+ process.stderr.write(
1981
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
1982
+ `
1983
+ );
1984
+ }
1893
1985
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1894
1986
  return;
1895
1987
  }
1896
1988
  if (platform === "goose") {
1989
+ try {
1990
+ const result = await mergeGooseConfig(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
1991
+ if (result === "ok") {
1992
+ printHostedProxyPostWriteHints(platform, shortName);
1993
+ return;
1994
+ }
1995
+ } catch (err) {
1996
+ process.stderr.write(
1997
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
1998
+ `
1999
+ );
2000
+ }
1897
2001
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1898
2002
  return;
1899
2003
  }
@@ -1940,12 +2044,19 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1940
2044
  apiKey
1941
2045
  );
1942
2046
  if (result === "parse-error") {
1943
- printHostedProxyJsonParseWarning(join(workspacePath, ".kilocode", "mcp.json"));
2047
+ printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
1944
2048
  }
1945
2049
  } else if (platform === "continue-dev") {
1946
- result = await mergeContinueHostedMcp(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2050
+ result = await mergeContinueHostedMcp(
2051
+ workspacePath,
2052
+ shortName,
2053
+ proxyUrlWithKeyWhenNeeded,
2054
+ apiKey
2055
+ );
1947
2056
  if (result === "parse-error") {
1948
- printHostedProxyJsonParseWarning(getContinueConfigJsonPath());
2057
+ printHostedProxyJsonParseWarning(
2058
+ join(workspacePath, ".continue", "mcpServers", `${shortName}.yaml`)
2059
+ );
1949
2060
  }
1950
2061
  } else {
1951
2062
  result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
@@ -1969,17 +2080,96 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1969
2080
  }
1970
2081
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1971
2082
  }
1972
- function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
1973
- return `extensions:
1974
- ${shortName}:
2083
+ function printGooseConfigYamlParseErrorToStderr() {
2084
+ process.stderr.write(
2085
+ style.yellow("!") + " Could not parse ~/.config/goose/config.yaml - check for syntax errors or invalid YAML\n"
2086
+ );
2087
+ }
2088
+ function gooseExtensionYaml(shortName, proxyUrl, bearerHeader) {
2089
+ const sn = sanitiseYamlValue(shortName);
2090
+ const urlEsc = sanitiseYamlValue(proxyUrl);
2091
+ const authEsc = sanitiseYamlValue(bearerHeader);
2092
+ return ` ${sn}:
2093
+ enabled: true
1975
2094
  type: streamable_http
1976
- url: ${proxyUrl}
2095
+ name: ${sn}
2096
+ description: ''
2097
+ uri: ${urlEsc}
2098
+ envs: {}
2099
+ env_keys: []
1977
2100
  headers:
1978
- Authorization: ${bearerHeader}
1979
- enabled: true
2101
+ Authorization: ${authEsc}
1980
2102
  timeout: 300
2103
+ socket: null
2104
+ bundled: null
2105
+ available_tools: []
1981
2106
  `;
1982
2107
  }
2108
+ function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
2109
+ return `extensions:
2110
+ ` + gooseExtensionYaml(shortName, proxyUrl, bearerHeader);
2111
+ }
2112
+ function isYamlPlainObject(value) {
2113
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2114
+ }
2115
+ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2116
+ const filePath = join(homedir(), ".config", "goose", "config.yaml");
2117
+ const bearerHeader = `Bearer ${apiKey}`;
2118
+ let content = "";
2119
+ try {
2120
+ content = await readFile(filePath, "utf8");
2121
+ } catch (e) {
2122
+ if (isErrnoException(e) && e.code === "ENOENT") {
2123
+ content = "";
2124
+ } else {
2125
+ throw e;
2126
+ }
2127
+ }
2128
+ let root;
2129
+ try {
2130
+ const data = content.trim().length === 0 ? {} : parse(content);
2131
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
2132
+ printGooseConfigYamlParseErrorToStderr();
2133
+ return "parse-error";
2134
+ }
2135
+ root = data;
2136
+ } catch {
2137
+ printGooseConfigYamlParseErrorToStderr();
2138
+ return "parse-error";
2139
+ }
2140
+ const extensionsRaw = root["extensions"];
2141
+ let extensions;
2142
+ if (isYamlPlainObject(extensionsRaw)) {
2143
+ extensions = { ...extensionsRaw };
2144
+ } else if (extensionsRaw === void 0) {
2145
+ extensions = {};
2146
+ } else {
2147
+ printGooseConfigYamlParseErrorToStderr();
2148
+ return "parse-error";
2149
+ }
2150
+ extensions[shortName] = {
2151
+ enabled: true,
2152
+ type: "streamable_http",
2153
+ name: shortName,
2154
+ description: "",
2155
+ uri: proxyUrl,
2156
+ envs: {},
2157
+ env_keys: [],
2158
+ headers: { Authorization: bearerHeader },
2159
+ timeout: 300,
2160
+ socket: null,
2161
+ bundled: null,
2162
+ available_tools: []
2163
+ };
2164
+ root["extensions"] = extensions;
2165
+ const out = stringify(root, { indent: 2, lineWidth: 0 });
2166
+ const body = out.endsWith("\n") ? out : `${out}
2167
+ `;
2168
+ await mkdir(dirname(filePath), { recursive: true });
2169
+ await writeFile(filePath, body, SECRET_JSON_FILE_OPTIONS);
2170
+ writeMcpAddedLine(shortName, filePath);
2171
+ return "ok";
2172
+ }
1983
2173
  function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1984
2174
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
1985
2175
  "cursor",
@@ -1999,14 +2189,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1999
2189
  if (platform === "github-copilot") {
2000
2190
  snippetText = JSON.stringify(
2001
2191
  {
2002
- mcp: {
2003
- servers: {
2004
- [shortName]: {
2005
- type: "http",
2006
- url: urlInSnippet,
2007
- headers: {
2008
- Authorization: authHeader
2009
- }
2192
+ servers: {
2193
+ [shortName]: {
2194
+ type: "http",
2195
+ url: urlInSnippet,
2196
+ headers: {
2197
+ Authorization: authHeader
2010
2198
  }
2011
2199
  }
2012
2200
  }
@@ -2045,18 +2233,32 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2045
2233
  2
2046
2234
  );
2047
2235
  } else if (platform === "continue-dev") {
2236
+ const sn = sanitiseYamlValue(shortName);
2237
+ const urlEsc = sanitiseYamlValue(urlInSnippet);
2238
+ const authEsc = sanitiseYamlValue(authHeader);
2239
+ snippetText = `name: ${sn}
2240
+ version: 0.0.1
2241
+ schema: v1
2242
+ mcpServers:
2243
+ - name: ${sn}
2244
+ type: streamable-http
2245
+ url: ${urlEsc}
2246
+ headers:
2247
+ Authorization: ${authEsc}
2248
+ `;
2249
+ } else if (platform === "kilo-code") {
2048
2250
  snippetText = JSON.stringify(
2049
2251
  {
2050
- mcpServers: [
2051
- {
2052
- name: shortName,
2053
- type: "streamable-http",
2252
+ mcp: {
2253
+ [shortName]: {
2254
+ type: "remote",
2054
2255
  url: urlInSnippet,
2055
2256
  headers: {
2056
2257
  Authorization: authHeader
2057
- }
2258
+ },
2259
+ enabled: true
2058
2260
  }
2059
- ]
2261
+ }
2060
2262
  },
2061
2263
  null,
2062
2264
  2
@@ -2096,16 +2298,18 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2096
2298
  );
2097
2299
  } else if (platform === "kilo-code") {
2098
2300
  process.stderr.write(
2099
- "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilocode", "mcp.json")}:`) + "\n\n"
2301
+ "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
2100
2302
  );
2101
2303
  } else if (platform === "github-copilot") {
2102
2304
  process.stderr.write(
2103
2305
  "\n" + style.dim(
2104
- "Merge this snippet under the mcp key in your VS Code Settings (JSON). If you do not have an mcp section yet, add one. Copilot picks up MCP servers when you use Agent mode."
2306
+ "Create .vscode/mcp.json in your workspace root (create the .vscode folder if it does not exist). After saving, reload VS Code and confirm the server appears in Copilot Agent mode under Tools."
2105
2307
  ) + "\n\n"
2106
2308
  );
2107
2309
  } else if (platform === "continue-dev") {
2108
- process.stderr.write("\n" + style.dim(`Add this to ${getContinueConfigJsonPath()}:`) + "\n\n");
2310
+ process.stderr.write(
2311
+ "\n" + style.dim(`Save this as .continue/mcpServers/${shortName}.yaml in your workspace root.`) + "\n\n"
2312
+ );
2109
2313
  } else if (platform === "goose") {
2110
2314
  process.stderr.write(
2111
2315
  "\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
@@ -2282,7 +2486,7 @@ async function runInit(explicitBaseUrl, options) {
2282
2486
  }
2283
2487
  await warnIfInstalledShieldIsOutdated();
2284
2488
  const configuredAgents = [];
2285
- let currentAgents = collectAgentsFromConfig(existing);
2489
+ let currentAgents = mergeAgentsForUniqueNames(collectAgentsFromConfig(existing));
2286
2490
  let lastConfig = {
2287
2491
  apiKey,
2288
2492
  baseUrl: resolvedBaseUrl,
@@ -2943,23 +3147,27 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2943
3147
  );
2944
3148
  }
2945
3149
  if (configuredPlatforms.has("kilo-code")) {
3150
+ const kiloLabel = mcpPromptLabel2("kilo-code");
2946
3151
  blocks.push(
2947
- "\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Try it: make a request in Kilo Code - Shield will intercept the first tool call and ask for your consent\n"
3152
+ "\n" + style.bold("Kilo Code") + '\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Confirm connection: Settings \u2192 Agent Behaviour \u2192 MCP Servers\n \u2192 Try it: paste this into Kilo Code:\n "Use the ' + kiloLabel + ' MCP server to list my GitHub repositories"\n'
2948
3153
  );
2949
3154
  }
2950
3155
  if (configuredPlatforms.has("github-copilot")) {
3156
+ const copilotLabel = mcpPromptLabel2("github-copilot");
2951
3157
  blocks.push(
2952
- "\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Open Copilot Agent mode and confirm the MCP server connects\n \u2192 Try it: make a request in GitHub Copilot - Shield will intercept the first tool call and ask for your consent\n"
3158
+ "\n" + style.bold("GitHub Copilot") + '\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Confirm connection: open Copilot chat in Agent mode and confirm the server appears under Tools\n \u2192 Try it: paste this into GitHub Copilot:\n "Use the ' + copilotLabel + ' MCP server to list my GitHub repositories"\n'
2953
3159
  );
2954
3160
  }
2955
3161
  if (configuredPlatforms.has("continue-dev")) {
3162
+ const continueLabel = mcpPromptLabel2("continue-dev");
2956
3163
  blocks.push(
2957
- "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Try it: make a request in Continue - Shield will intercept the first tool call and ask for your consent\n"
3164
+ "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + '\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Confirm connection: Settings \u2192 Tools \u2192 MCP Servers\n \u2192 Try it: paste this into Continue:\n "Use the ' + continueLabel + ' MCP server to list my GitHub repositories"\n'
2958
3165
  );
2959
3166
  }
2960
3167
  if (configuredPlatforms.has("goose")) {
3168
+ const gooseLabel = mcpPromptLabel2("goose");
2961
3169
  blocks.push(
2962
- "\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n \u2192 Try it: make a request in Goose - Shield will intercept the first tool call and ask for your consent\n"
3170
+ "\n" + style.bold("Goose") + '\n \u2192 Start a new Goose session after updating config\n \u2192 Confirm connection: check the Extensions page in the sidebar\n \u2192 Try it: paste this into Goose:\n "Use the ' + gooseLabel + ' MCP server to list my GitHub repositories"\n'
2963
3171
  );
2964
3172
  }
2965
3173
  const windsurfNativeConfigured = configuredAgents.some(
@@ -3915,7 +4123,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
3915
4123
 
3916
4124
  // package.json
3917
4125
  var package_default = {
3918
- version: "1.6.0"};
4126
+ version: "1.7.0"};
3919
4127
 
3920
4128
  // src/package-meta.ts
3921
4129
  var PACKAGE_VERSION = package_default.version;
@@ -10,6 +10,7 @@ import { createHash } from 'crypto';
10
10
  import 'url';
11
11
  import 'module';
12
12
  import 'readline';
13
+ import 'yaml';
13
14
 
14
15
  // Multicorn Shield Claude Desktop Extension - https://multicorn.ai
15
16
  var __create = Object.create;
@@ -22504,7 +22505,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22504
22505
 
22505
22506
  // package.json
22506
22507
  var package_default = {
22507
- version: "1.6.0"};
22508
+ version: "1.7.0"};
22508
22509
 
22509
22510
  // src/package-meta.ts
22510
22511
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "author": "Multicorn AI Pty Ltd",
@@ -69,6 +69,7 @@
69
69
  "dependencies": {
70
70
  "@modelcontextprotocol/sdk": "^1.27.1",
71
71
  "lit": "^3.2.0",
72
+ "yaml": "^2.8.2",
72
73
  "zod": "^4.3.6"
73
74
  },
74
75
  "devDependencies": {