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 +19 -1
- package/dist/multicorn-proxy.js +287 -78
- package/dist/multicorn-shield.js +286 -78
- package/dist/shield-extension.js +2 -1
- package/package.json +2 -1
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.
|
|
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
|
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
|
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
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ".
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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 .
|
|
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
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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, ".
|
|
1967
|
+
printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
|
|
1864
1968
|
}
|
|
1865
1969
|
} else if (platform === "continue-dev") {
|
|
1866
|
-
result = await mergeContinueHostedMcp(
|
|
1970
|
+
result = await mergeContinueHostedMcp(
|
|
1971
|
+
workspacePath,
|
|
1972
|
+
shortName,
|
|
1973
|
+
proxyUrlWithKeyWhenNeeded,
|
|
1974
|
+
apiKey
|
|
1975
|
+
);
|
|
1867
1976
|
if (result === "parse-error") {
|
|
1868
|
-
printHostedProxyJsonParseWarning(
|
|
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
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
2015
|
+
name: ${sn}
|
|
2016
|
+
description: ''
|
|
2017
|
+
uri: ${urlEsc}
|
|
2018
|
+
envs: {}
|
|
2019
|
+
env_keys: []
|
|
1897
2020
|
headers:
|
|
1898
|
-
Authorization: ${
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
1971
|
-
{
|
|
1972
|
-
|
|
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()), ".
|
|
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
|
-
"
|
|
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(
|
|
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") +
|
|
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") +
|
|
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") +
|
|
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") +
|
|
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.
|
|
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: {
|
package/dist/multicorn-shield.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
|
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
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ".
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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 .
|
|
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
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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, ".
|
|
2047
|
+
printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
|
|
1944
2048
|
}
|
|
1945
2049
|
} else if (platform === "continue-dev") {
|
|
1946
|
-
result = await mergeContinueHostedMcp(
|
|
2050
|
+
result = await mergeContinueHostedMcp(
|
|
2051
|
+
workspacePath,
|
|
2052
|
+
shortName,
|
|
2053
|
+
proxyUrlWithKeyWhenNeeded,
|
|
2054
|
+
apiKey
|
|
2055
|
+
);
|
|
1947
2056
|
if (result === "parse-error") {
|
|
1948
|
-
printHostedProxyJsonParseWarning(
|
|
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
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
2095
|
+
name: ${sn}
|
|
2096
|
+
description: ''
|
|
2097
|
+
uri: ${urlEsc}
|
|
2098
|
+
envs: {}
|
|
2099
|
+
env_keys: []
|
|
1977
2100
|
headers:
|
|
1978
|
-
Authorization: ${
|
|
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
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
-
|
|
2051
|
-
{
|
|
2052
|
-
|
|
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()), ".
|
|
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
|
-
"
|
|
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(
|
|
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") +
|
|
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") +
|
|
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") +
|
|
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") +
|
|
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.
|
|
4126
|
+
version: "1.7.0"};
|
|
3919
4127
|
|
|
3920
4128
|
// src/package-meta.ts
|
|
3921
4129
|
var PACKAGE_VERSION = package_default.version;
|
package/dist/shield-extension.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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": {
|