@ai-ecoverse/mcpcat 0.4.0 → 0.6.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/dist/host/mcpcat.mjs +1277 -254
- package/dist/kernel/mcpcat.mjs +1269 -248
- package/package.json +6 -5
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -1708,6 +1708,185 @@ var StreamableHttpTransport = class extends TransportEvents {
|
|
|
1708
1708
|
}
|
|
1709
1709
|
};
|
|
1710
1710
|
|
|
1711
|
+
// src/client/platform-stdio-transport.ts
|
|
1712
|
+
var DEFAULT_MAX_MESSAGE_BYTES2 = 4 * 1024 * 1024;
|
|
1713
|
+
var DEFAULT_MAX_STDERR_BYTES2 = 64 * 1024;
|
|
1714
|
+
var DEFAULT_CLOSE_TIMEOUT_MS2 = 2e3;
|
|
1715
|
+
var STDIN_CLOSE_GRACE_MS2 = 500;
|
|
1716
|
+
var PlatformStdioTransport = class {
|
|
1717
|
+
options;
|
|
1718
|
+
process;
|
|
1719
|
+
started = false;
|
|
1720
|
+
closed = false;
|
|
1721
|
+
closeEmitted = false;
|
|
1722
|
+
stderrText = "";
|
|
1723
|
+
decoder = new TextDecoder();
|
|
1724
|
+
encoder = new TextEncoder();
|
|
1725
|
+
messageListeners = [];
|
|
1726
|
+
errorListeners = [];
|
|
1727
|
+
closeListeners = [];
|
|
1728
|
+
constructor(options) {
|
|
1729
|
+
this.options = options;
|
|
1730
|
+
}
|
|
1731
|
+
get stderr() {
|
|
1732
|
+
return this.stderrText;
|
|
1733
|
+
}
|
|
1734
|
+
onMessage(listener) {
|
|
1735
|
+
this.messageListeners.push(listener);
|
|
1736
|
+
return () => {
|
|
1737
|
+
this.messageListeners = this.messageListeners.filter((l) => l !== listener);
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
onError(listener) {
|
|
1741
|
+
this.errorListeners.push(listener);
|
|
1742
|
+
return () => {
|
|
1743
|
+
this.errorListeners = this.errorListeners.filter((l) => l !== listener);
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
onClose(listener) {
|
|
1747
|
+
this.closeListeners.push(listener);
|
|
1748
|
+
return () => {
|
|
1749
|
+
this.closeListeners = this.closeListeners.filter((l) => l !== listener);
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
emitMessage(message) {
|
|
1753
|
+
for (const l of [...this.messageListeners]) l(message);
|
|
1754
|
+
}
|
|
1755
|
+
emitError(error) {
|
|
1756
|
+
for (const l of [...this.errorListeners]) l(error);
|
|
1757
|
+
}
|
|
1758
|
+
emitClose() {
|
|
1759
|
+
if (this.closeEmitted) return;
|
|
1760
|
+
this.closeEmitted = true;
|
|
1761
|
+
for (const l of [...this.closeListeners]) l();
|
|
1762
|
+
}
|
|
1763
|
+
async start() {
|
|
1764
|
+
if (this.started) throw new Error("MCP stdio transport already started");
|
|
1765
|
+
if (this.closed) throw new Error("MCP connection closed");
|
|
1766
|
+
this.started = true;
|
|
1767
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env ?? {} } : {
|
|
1768
|
+
...this.options.platform.env,
|
|
1769
|
+
...this.options.env
|
|
1770
|
+
};
|
|
1771
|
+
this.process = await this.options.platform.spawn({
|
|
1772
|
+
argv: [this.options.command, ...this.options.args ?? []],
|
|
1773
|
+
env,
|
|
1774
|
+
cwd: this.options.cwd
|
|
1775
|
+
});
|
|
1776
|
+
void this.readStdout();
|
|
1777
|
+
void this.readStderr();
|
|
1778
|
+
void this.waitForExit();
|
|
1779
|
+
}
|
|
1780
|
+
async send(message) {
|
|
1781
|
+
if (!this.started || this.closed || !this.process) throw new Error("MCP connection closed");
|
|
1782
|
+
const payload = `${JSON.stringify(message)}
|
|
1783
|
+
`;
|
|
1784
|
+
const writer = this.process.stdin.getWriter();
|
|
1785
|
+
try {
|
|
1786
|
+
await writer.write(this.encoder.encode(payload));
|
|
1787
|
+
} finally {
|
|
1788
|
+
writer.releaseLock();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
async close() {
|
|
1792
|
+
if (this.closed) return;
|
|
1793
|
+
this.closed = true;
|
|
1794
|
+
const proc = this.process;
|
|
1795
|
+
if (!proc) {
|
|
1796
|
+
this.emitClose();
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS2;
|
|
1800
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS2, closeTimeoutMs);
|
|
1801
|
+
const done = proc.wait().then(() => true);
|
|
1802
|
+
const delay = (ms) => new Promise((r) => setTimeout(() => r(false), ms));
|
|
1803
|
+
try {
|
|
1804
|
+
await proc.stdin.close();
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
const exited = await Promise.race([done, delay(grace)]);
|
|
1808
|
+
if (!exited) {
|
|
1809
|
+
proc.kill("SIGTERM");
|
|
1810
|
+
const exitedAfterTerm = await Promise.race([done, delay(closeTimeoutMs)]);
|
|
1811
|
+
if (!exitedAfterTerm) {
|
|
1812
|
+
proc.kill("SIGKILL");
|
|
1813
|
+
await done;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
proc.kill("SIGTERM");
|
|
1817
|
+
this.emitClose();
|
|
1818
|
+
}
|
|
1819
|
+
async readStdout() {
|
|
1820
|
+
const proc = this.process;
|
|
1821
|
+
if (!proc) return;
|
|
1822
|
+
const reader = proc.stdout.getReader();
|
|
1823
|
+
const maxBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES2;
|
|
1824
|
+
let buffer = "";
|
|
1825
|
+
try {
|
|
1826
|
+
for (; ; ) {
|
|
1827
|
+
const { done, value } = await reader.read();
|
|
1828
|
+
if (done) break;
|
|
1829
|
+
buffer += this.decoder.decode(value, { stream: true });
|
|
1830
|
+
let nlIdx = buffer.indexOf("\n");
|
|
1831
|
+
while (nlIdx >= 0) {
|
|
1832
|
+
const line = buffer.slice(0, nlIdx).replace(/\r$/, "");
|
|
1833
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
1834
|
+
if (line.trim()) {
|
|
1835
|
+
try {
|
|
1836
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(line)));
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
this.emitError(err);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
nlIdx = buffer.indexOf("\n");
|
|
1842
|
+
}
|
|
1843
|
+
if (this.encoder.encode(buffer).length > maxBytes) {
|
|
1844
|
+
buffer = "";
|
|
1845
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxBytes} bytes`));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
buffer += this.decoder.decode();
|
|
1849
|
+
if (buffer.trim()) {
|
|
1850
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
1851
|
+
}
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
if (!this.closed) this.emitError(err);
|
|
1854
|
+
} finally {
|
|
1855
|
+
reader.releaseLock();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async readStderr() {
|
|
1859
|
+
const proc = this.process;
|
|
1860
|
+
if (!proc) return;
|
|
1861
|
+
const reader = proc.stderr.getReader();
|
|
1862
|
+
const maxBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES2;
|
|
1863
|
+
try {
|
|
1864
|
+
for (; ; ) {
|
|
1865
|
+
const { done, value } = await reader.read();
|
|
1866
|
+
if (done) break;
|
|
1867
|
+
const chunk = this.decoder.decode(value, { stream: true });
|
|
1868
|
+
this.stderrText += chunk;
|
|
1869
|
+
if (this.encoder.encode(this.stderrText).length > maxBytes) {
|
|
1870
|
+
this.stderrText = this.stderrText.slice(-maxBytes);
|
|
1871
|
+
}
|
|
1872
|
+
this.options.onStderr?.(chunk);
|
|
1873
|
+
}
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async waitForExit() {
|
|
1878
|
+
const proc = this.process;
|
|
1879
|
+
if (!proc) return;
|
|
1880
|
+
try {
|
|
1881
|
+
await proc.wait();
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
if (!this.closed) {
|
|
1885
|
+
this.emitClose();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1711
1890
|
// src/client/pi-connection.ts
|
|
1712
1891
|
async function createPiConnection(options) {
|
|
1713
1892
|
const clientOptions = {
|
|
@@ -1719,17 +1898,29 @@ async function createPiConnection(options) {
|
|
|
1719
1898
|
if (options.url) {
|
|
1720
1899
|
transport = new StreamableHttpTransport({
|
|
1721
1900
|
url: options.url,
|
|
1722
|
-
headers: options.headers
|
|
1901
|
+
headers: options.headers,
|
|
1902
|
+
fetch: options.fetchImpl
|
|
1723
1903
|
});
|
|
1724
1904
|
} else if (options.command) {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1905
|
+
if (options.platform) {
|
|
1906
|
+
transport = new PlatformStdioTransport({
|
|
1907
|
+
command: options.command,
|
|
1908
|
+
args: options.args,
|
|
1909
|
+
env: options.env,
|
|
1910
|
+
cwd: options.cwd,
|
|
1911
|
+
inheritEnv: true,
|
|
1912
|
+
platform: options.platform
|
|
1913
|
+
});
|
|
1914
|
+
} else {
|
|
1915
|
+
transport = new StdioTransport({
|
|
1916
|
+
command: options.command,
|
|
1917
|
+
args: options.args,
|
|
1918
|
+
env: options.env,
|
|
1919
|
+
cwd: options.cwd,
|
|
1920
|
+
inheritEnv: true,
|
|
1921
|
+
stderr: "pipe"
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1733
1924
|
} else {
|
|
1734
1925
|
throw new Error("PiConnection requires either a url or a command");
|
|
1735
1926
|
}
|
|
@@ -1817,10 +2008,10 @@ async function createStatelessConnection(options) {
|
|
|
1817
2008
|
timeoutMs = 6e4,
|
|
1818
2009
|
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
1819
2010
|
} = options;
|
|
1820
|
-
let
|
|
2011
|
+
let nextId2 = 1;
|
|
1821
2012
|
let negotiatedVersion;
|
|
1822
2013
|
async function request(method, params) {
|
|
1823
|
-
const id =
|
|
2014
|
+
const id = nextId2++;
|
|
1824
2015
|
const headers = {
|
|
1825
2016
|
"Content-Type": "application/json",
|
|
1826
2017
|
Accept: "application/json, text/event-stream",
|
|
@@ -1943,7 +2134,8 @@ async function connect(platform2, options) {
|
|
|
1943
2134
|
args: options.args,
|
|
1944
2135
|
env: options.env,
|
|
1945
2136
|
cwd: options.cwd,
|
|
1946
|
-
version: options.version
|
|
2137
|
+
version: options.version,
|
|
2138
|
+
platform: platform2
|
|
1947
2139
|
});
|
|
1948
2140
|
}
|
|
1949
2141
|
if (!options.url) {
|
|
@@ -1953,7 +2145,9 @@ async function connect(platform2, options) {
|
|
|
1953
2145
|
return createPiConnection({
|
|
1954
2146
|
url: options.url,
|
|
1955
2147
|
headers: options.headers,
|
|
1956
|
-
version: options.version
|
|
2148
|
+
version: options.version,
|
|
2149
|
+
fetchImpl: platform2.fetch,
|
|
2150
|
+
platform: platform2
|
|
1957
2151
|
});
|
|
1958
2152
|
}
|
|
1959
2153
|
try {
|
|
@@ -1972,7 +2166,9 @@ async function connect(platform2, options) {
|
|
|
1972
2166
|
return createPiConnection({
|
|
1973
2167
|
url: options.url,
|
|
1974
2168
|
headers: options.headers,
|
|
1975
|
-
version: options.version
|
|
2169
|
+
version: options.version,
|
|
2170
|
+
fetchImpl: platform2.fetch,
|
|
2171
|
+
platform: platform2
|
|
1976
2172
|
});
|
|
1977
2173
|
}
|
|
1978
2174
|
throw err;
|
|
@@ -2325,73 +2521,989 @@ async function loadConfig(input) {
|
|
|
2325
2521
|
if (parsed.autoEnableCodemode !== void 0) autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2326
2522
|
loadUserServers(parsed, input.userPath, servers, problems);
|
|
2327
2523
|
}
|
|
2328
|
-
if (input.projectPath && input.trustProject) {
|
|
2329
|
-
const projectText = await readFile(input.fs, input.projectPath);
|
|
2330
|
-
if (projectText !== void 0) {
|
|
2331
|
-
const parsed = parseMcpJson(projectText);
|
|
2332
|
-
if (parsed.autoEnableCodemode !== void 0) autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2333
|
-
loadProjectServers(parsed, input.projectPath, servers, problems);
|
|
2524
|
+
if (input.projectPath && input.trustProject) {
|
|
2525
|
+
const projectText = await readFile(input.fs, input.projectPath);
|
|
2526
|
+
if (projectText !== void 0) {
|
|
2527
|
+
const parsed = parseMcpJson(projectText);
|
|
2528
|
+
if (parsed.autoEnableCodemode !== void 0) autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2529
|
+
loadProjectServers(parsed, input.projectPath, servers, problems);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
servers: [...servers.values()],
|
|
2534
|
+
problems,
|
|
2535
|
+
...autoEnableCodemode !== void 0 ? { autoEnableCodemode } : {}
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// src/config/paths.ts
|
|
2540
|
+
function userConfigDir(platform2) {
|
|
2541
|
+
const piDir = platform2.env.PI_CODING_AGENT_DIR;
|
|
2542
|
+
if (piDir) return piDir;
|
|
2543
|
+
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "/";
|
|
2544
|
+
return `${home}/.pi/agent`;
|
|
2545
|
+
}
|
|
2546
|
+
function userConfigPath(platform2) {
|
|
2547
|
+
return `${userConfigDir(platform2)}/mcp.json`;
|
|
2548
|
+
}
|
|
2549
|
+
function projectConfigPath(platform2) {
|
|
2550
|
+
return `${platform2.cwd}/.pi/mcp.json`;
|
|
2551
|
+
}
|
|
2552
|
+
function stateDir(platform2) {
|
|
2553
|
+
if (platform2.env.XDG_STATE_HOME) return `${platform2.env.XDG_STATE_HOME}/mcpcat`;
|
|
2554
|
+
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "/";
|
|
2555
|
+
if (platform2.env.LOCALAPPDATA) return `${platform2.env.LOCALAPPDATA}\\mcpcat\\state`;
|
|
2556
|
+
if (platform2.env.SLICC_KERNEL) return `${home}/.local/state/mcpcat`;
|
|
2557
|
+
return `${home}/Library/Application Support/mcpcat`;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/config/index.ts
|
|
2561
|
+
var MergedConfigSource = class _MergedConfigSource {
|
|
2562
|
+
entries;
|
|
2563
|
+
config;
|
|
2564
|
+
constructor(config) {
|
|
2565
|
+
this.config = config;
|
|
2566
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2567
|
+
for (const entry of config.servers) {
|
|
2568
|
+
this.entries.set(entry.name, entry);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
async get(name) {
|
|
2572
|
+
const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
|
|
2573
|
+
for (const [key, entry] of this.entries) {
|
|
2574
|
+
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
2575
|
+
return entry.config;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return void 0;
|
|
2579
|
+
}
|
|
2580
|
+
entry(name) {
|
|
2581
|
+
return this.entries.get(name);
|
|
2582
|
+
}
|
|
2583
|
+
list() {
|
|
2584
|
+
return [...this.entries.values()];
|
|
2585
|
+
}
|
|
2586
|
+
static async load(platform2, options) {
|
|
2587
|
+
const userPath = options?.configFile ?? userConfigPath(platform2);
|
|
2588
|
+
const projectPath = options?.configFile ? void 0 : projectConfigPath(platform2);
|
|
2589
|
+
const config = await loadConfig({
|
|
2590
|
+
userPath,
|
|
2591
|
+
projectPath,
|
|
2592
|
+
trustProject: options?.trustProject ?? false,
|
|
2593
|
+
fs: platform2.fs
|
|
2594
|
+
});
|
|
2595
|
+
return new _MergedConfigSource(config);
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// src/config/lock.ts
|
|
2600
|
+
var LOCK_STALE_MS = 1e4;
|
|
2601
|
+
var LOCK_POLL_MS = 50;
|
|
2602
|
+
var LOCK_MAX_WAIT_MS = 5e3;
|
|
2603
|
+
function isEexist(err) {
|
|
2604
|
+
if (err instanceof Error && "code" in err)
|
|
2605
|
+
return err.code === "EEXIST";
|
|
2606
|
+
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
async function removeStaleLock(fs, lockPath) {
|
|
2610
|
+
try {
|
|
2611
|
+
const s = await fs.stat(lockPath);
|
|
2612
|
+
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
2613
|
+
try {
|
|
2614
|
+
await fs.rmdir(lockPath);
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
async function acquireLock(fs, configPath) {
|
|
2624
|
+
const lockPath = `${configPath}.lock`;
|
|
2625
|
+
const start = Date.now();
|
|
2626
|
+
while (true) {
|
|
2627
|
+
try {
|
|
2628
|
+
await fs.mkdirExclusive(lockPath);
|
|
2629
|
+
break;
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
if (!isEexist(err)) throw err;
|
|
2632
|
+
await removeStaleLock(fs, lockPath);
|
|
2633
|
+
}
|
|
2634
|
+
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
2635
|
+
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
2636
|
+
}
|
|
2637
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
2638
|
+
}
|
|
2639
|
+
let released = false;
|
|
2640
|
+
return async () => {
|
|
2641
|
+
if (released) return;
|
|
2642
|
+
released = true;
|
|
2643
|
+
try {
|
|
2644
|
+
await fs.rmdir(lockPath);
|
|
2645
|
+
} catch {
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/config/edit.ts
|
|
2651
|
+
function isRecord2(value) {
|
|
2652
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2653
|
+
}
|
|
2654
|
+
function detectIndent(text) {
|
|
2655
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
2656
|
+
return match ? match[1] : " ";
|
|
2657
|
+
}
|
|
2658
|
+
async function readText(fs, path) {
|
|
2659
|
+
try {
|
|
2660
|
+
const data = await fs.readFile(path);
|
|
2661
|
+
return new TextDecoder().decode(data);
|
|
2662
|
+
} catch {
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async function editMcpServers(fs, path, edit) {
|
|
2667
|
+
const release = await acquireLock(fs, path);
|
|
2668
|
+
try {
|
|
2669
|
+
const text = await readText(fs, path);
|
|
2670
|
+
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
2671
|
+
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
2672
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
2673
|
+
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
2674
|
+
if (!edit(servers, parsed)) return { changed: false };
|
|
2675
|
+
const indent = text ? detectIndent(text) : " ";
|
|
2676
|
+
const output = `${JSON.stringify(parsed, null, indent)}
|
|
2677
|
+
`;
|
|
2678
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
2679
|
+
if (dir) {
|
|
2680
|
+
try {
|
|
2681
|
+
await fs.mkdir(dir);
|
|
2682
|
+
} catch {
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
2686
|
+
await fs.writeFile(tmpPath, output);
|
|
2687
|
+
await fs.rename(tmpPath, path);
|
|
2688
|
+
return { changed: true };
|
|
2689
|
+
} finally {
|
|
2690
|
+
await release();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
async function addServer(fs, path, name, config) {
|
|
2694
|
+
let replaced = false;
|
|
2695
|
+
await editMcpServers(fs, path, (servers, parsed) => {
|
|
2696
|
+
const target = servers ?? {};
|
|
2697
|
+
replaced = target[name] !== void 0;
|
|
2698
|
+
target[name] = config;
|
|
2699
|
+
parsed.mcpServers = target;
|
|
2700
|
+
return true;
|
|
2701
|
+
});
|
|
2702
|
+
return { replaced };
|
|
2703
|
+
}
|
|
2704
|
+
async function removeServer(fs, path, name) {
|
|
2705
|
+
let removed = false;
|
|
2706
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2707
|
+
if (!servers || servers[name] === void 0) return false;
|
|
2708
|
+
delete servers[name];
|
|
2709
|
+
removed = true;
|
|
2710
|
+
return true;
|
|
2711
|
+
});
|
|
2712
|
+
return removed;
|
|
2713
|
+
}
|
|
2714
|
+
async function updateServer(fs, path, name, patch) {
|
|
2715
|
+
let updated = false;
|
|
2716
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2717
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2718
|
+
const entry = servers[name];
|
|
2719
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
2720
|
+
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
2721
|
+
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
2722
|
+
updated = true;
|
|
2723
|
+
return true;
|
|
2724
|
+
});
|
|
2725
|
+
return updated;
|
|
2726
|
+
}
|
|
2727
|
+
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
2728
|
+
let updated = false;
|
|
2729
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2730
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2731
|
+
const entry = servers[name];
|
|
2732
|
+
if (toolGlob) {
|
|
2733
|
+
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
2734
|
+
te[toolGlob] = exposure;
|
|
2735
|
+
entry.toolExposure = te;
|
|
2736
|
+
} else {
|
|
2737
|
+
entry.exposure = exposure;
|
|
2738
|
+
}
|
|
2739
|
+
updated = true;
|
|
2740
|
+
return true;
|
|
2741
|
+
});
|
|
2742
|
+
return updated;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/discovery/extract.ts
|
|
2746
|
+
var nextId = 1;
|
|
2747
|
+
function candidateId() {
|
|
2748
|
+
return String(nextId++);
|
|
2749
|
+
}
|
|
2750
|
+
function resetIdCounter(start = 1) {
|
|
2751
|
+
nextId = start;
|
|
2752
|
+
}
|
|
2753
|
+
function isRecord3(v) {
|
|
2754
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2755
|
+
}
|
|
2756
|
+
function checkUrnPublisher(identifier, originHost) {
|
|
2757
|
+
const urnMatch = /^urn:air:([^:]+):/.exec(identifier);
|
|
2758
|
+
if (!urnMatch) return void 0;
|
|
2759
|
+
const urnDomain = urnMatch[1];
|
|
2760
|
+
if (domainMatch(urnDomain, originHost)) {
|
|
2761
|
+
return { type: "urn-publisher", detail: `${urnDomain} matches ${originHost}` };
|
|
2762
|
+
}
|
|
2763
|
+
return { type: "urn-publisher-mismatch", detail: `${urnDomain} does not match ${originHost}` };
|
|
2764
|
+
}
|
|
2765
|
+
function checkTrustManifest(entry, originHost) {
|
|
2766
|
+
if (!isRecord3(entry.trustManifest) || typeof entry.trustManifest.identity !== "string")
|
|
2767
|
+
return void 0;
|
|
2768
|
+
const identity = entry.trustManifest.identity;
|
|
2769
|
+
if (domainMatch(identity, originHost)) {
|
|
2770
|
+
return { type: "trust-manifest", detail: `identity ${identity} matches origin` };
|
|
2771
|
+
}
|
|
2772
|
+
return {
|
|
2773
|
+
type: "trust-manifest-mismatch",
|
|
2774
|
+
detail: `identity ${identity} does not match ${originHost}`
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function extractArd(event) {
|
|
2778
|
+
const candidates = [];
|
|
2779
|
+
const summary = event.summary;
|
|
2780
|
+
if (!isRecord3(summary)) return candidates;
|
|
2781
|
+
const entries = summary.entries;
|
|
2782
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2783
|
+
for (const entry of entries) {
|
|
2784
|
+
if (!isRecord3(entry)) continue;
|
|
2785
|
+
if (entry.type !== "application/mcp-server-card+json") continue;
|
|
2786
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2787
|
+
if (!url) continue;
|
|
2788
|
+
const trust = [];
|
|
2789
|
+
const originHost = hostFromUrl(event.origin);
|
|
2790
|
+
const identifier = typeof entry.identifier === "string" ? entry.identifier : void 0;
|
|
2791
|
+
if (identifier) {
|
|
2792
|
+
const urnSignal = checkUrnPublisher(identifier, originHost);
|
|
2793
|
+
if (urnSignal) trust.push(urnSignal);
|
|
2794
|
+
}
|
|
2795
|
+
const tmSignal = checkTrustManifest(entry, originHost);
|
|
2796
|
+
if (tmSignal) trust.push(tmSignal);
|
|
2797
|
+
candidates.push({
|
|
2798
|
+
id: candidateId(),
|
|
2799
|
+
endpoint: url,
|
|
2800
|
+
standard: "ard",
|
|
2801
|
+
origin: event.origin,
|
|
2802
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2803
|
+
transport: "http",
|
|
2804
|
+
trust,
|
|
2805
|
+
status: event.status,
|
|
2806
|
+
reason: event.reason,
|
|
2807
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return candidates;
|
|
2811
|
+
}
|
|
2812
|
+
function extractAiCatalog(event) {
|
|
2813
|
+
const candidates = [];
|
|
2814
|
+
const summary = event.summary;
|
|
2815
|
+
if (!isRecord3(summary)) return candidates;
|
|
2816
|
+
const entries = summary.entries;
|
|
2817
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2818
|
+
for (const entry of entries) {
|
|
2819
|
+
if (!isRecord3(entry)) continue;
|
|
2820
|
+
if (entry.type !== "application/mcp-server-card+json" && entry.type !== "mcp-server") continue;
|
|
2821
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2822
|
+
if (!url) continue;
|
|
2823
|
+
candidates.push({
|
|
2824
|
+
id: candidateId(),
|
|
2825
|
+
endpoint: url,
|
|
2826
|
+
standard: "ai-catalog",
|
|
2827
|
+
origin: event.origin,
|
|
2828
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2829
|
+
transport: "http",
|
|
2830
|
+
trust: [],
|
|
2831
|
+
status: event.status,
|
|
2832
|
+
reason: event.reason,
|
|
2833
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
return candidates;
|
|
2837
|
+
}
|
|
2838
|
+
function extractUcp(event) {
|
|
2839
|
+
const candidates = [];
|
|
2840
|
+
const summary = event.summary;
|
|
2841
|
+
if (!isRecord3(summary)) return candidates;
|
|
2842
|
+
const services = summary.services;
|
|
2843
|
+
if (!isRecord3(services)) return candidates;
|
|
2844
|
+
for (const [svcName, svcValue] of Object.entries(services)) {
|
|
2845
|
+
if (!Array.isArray(svcValue)) continue;
|
|
2846
|
+
for (const transport of svcValue) {
|
|
2847
|
+
if (!isRecord3(transport)) continue;
|
|
2848
|
+
if (transport.transport !== "mcp") continue;
|
|
2849
|
+
const endpoint = typeof transport.endpoint === "string" ? transport.endpoint : void 0;
|
|
2850
|
+
if (!endpoint) continue;
|
|
2851
|
+
candidates.push({
|
|
2852
|
+
id: candidateId(),
|
|
2853
|
+
endpoint,
|
|
2854
|
+
standard: "ucp",
|
|
2855
|
+
origin: event.origin,
|
|
2856
|
+
name: svcName,
|
|
2857
|
+
transport: "http",
|
|
2858
|
+
trust: [],
|
|
2859
|
+
status: event.status,
|
|
2860
|
+
reason: event.reason,
|
|
2861
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return candidates;
|
|
2866
|
+
}
|
|
2867
|
+
function extractPoppy(event) {
|
|
2868
|
+
const candidates = [];
|
|
2869
|
+
const summary = event.summary;
|
|
2870
|
+
if (!isRecord3(summary)) return candidates;
|
|
2871
|
+
const apis = summary.apis;
|
|
2872
|
+
if (!Array.isArray(apis)) return candidates;
|
|
2873
|
+
for (const api of apis) {
|
|
2874
|
+
if (!isRecord3(api)) continue;
|
|
2875
|
+
if (api.type !== "mcp") continue;
|
|
2876
|
+
const url = typeof api.url === "string" ? api.url : void 0;
|
|
2877
|
+
if (!url) continue;
|
|
2878
|
+
candidates.push({
|
|
2879
|
+
id: candidateId(),
|
|
2880
|
+
endpoint: url,
|
|
2881
|
+
standard: "poppy",
|
|
2882
|
+
origin: event.origin,
|
|
2883
|
+
transport: "http",
|
|
2884
|
+
trust: [],
|
|
2885
|
+
status: event.status,
|
|
2886
|
+
reason: event.reason,
|
|
2887
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
return candidates;
|
|
2891
|
+
}
|
|
2892
|
+
function extractNlweb(event) {
|
|
2893
|
+
const summary = event.summary;
|
|
2894
|
+
if (!isRecord3(summary)) return [];
|
|
2895
|
+
const mcpUrl = typeof summary.mcpUrl === "string" ? summary.mcpUrl : void 0;
|
|
2896
|
+
if (!mcpUrl) return [];
|
|
2897
|
+
return [
|
|
2898
|
+
{
|
|
2899
|
+
id: candidateId(),
|
|
2900
|
+
endpoint: mcpUrl,
|
|
2901
|
+
standard: "nlweb",
|
|
2902
|
+
origin: event.origin,
|
|
2903
|
+
transport: "http",
|
|
2904
|
+
trust: [],
|
|
2905
|
+
status: event.status,
|
|
2906
|
+
reason: event.reason,
|
|
2907
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2908
|
+
}
|
|
2909
|
+
];
|
|
2910
|
+
}
|
|
2911
|
+
function extractWebmcp(event) {
|
|
2912
|
+
return [
|
|
2913
|
+
{
|
|
2914
|
+
id: candidateId(),
|
|
2915
|
+
endpoint: event.url,
|
|
2916
|
+
standard: "webmcp",
|
|
2917
|
+
origin: event.origin,
|
|
2918
|
+
transport: "webmcp",
|
|
2919
|
+
trust: [],
|
|
2920
|
+
status: event.status,
|
|
2921
|
+
reason: event.reason,
|
|
2922
|
+
suggestion: "mcpcat serve --webmcp",
|
|
2923
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2924
|
+
}
|
|
2925
|
+
];
|
|
2926
|
+
}
|
|
2927
|
+
function hostFromUrl(url) {
|
|
2928
|
+
try {
|
|
2929
|
+
return new URL(url).hostname;
|
|
2930
|
+
} catch {
|
|
2931
|
+
return "";
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
var TWO_LABEL_TLDS = /* @__PURE__ */ new Set([
|
|
2935
|
+
"co.uk",
|
|
2936
|
+
"org.uk",
|
|
2937
|
+
"ac.uk",
|
|
2938
|
+
"co.jp",
|
|
2939
|
+
"or.jp",
|
|
2940
|
+
"co.kr",
|
|
2941
|
+
"co.nz",
|
|
2942
|
+
"co.za",
|
|
2943
|
+
"com.au",
|
|
2944
|
+
"com.br",
|
|
2945
|
+
"com.cn",
|
|
2946
|
+
"com.mx",
|
|
2947
|
+
"com.tw",
|
|
2948
|
+
"net.au",
|
|
2949
|
+
"org.au",
|
|
2950
|
+
"co.in",
|
|
2951
|
+
"com.sg",
|
|
2952
|
+
"co.il"
|
|
2953
|
+
]);
|
|
2954
|
+
function registrableDomain(host) {
|
|
2955
|
+
const h = host.toLowerCase().replace(/^www\./, "");
|
|
2956
|
+
const parts = h.split(".");
|
|
2957
|
+
if (parts.length <= 2) return h;
|
|
2958
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
2959
|
+
if (TWO_LABEL_TLDS.has(lastTwo) && parts.length > 2) {
|
|
2960
|
+
return parts.slice(-3).join(".");
|
|
2961
|
+
}
|
|
2962
|
+
return lastTwo;
|
|
2963
|
+
}
|
|
2964
|
+
function domainMatch(a, b) {
|
|
2965
|
+
return registrableDomain(a) === registrableDomain(b);
|
|
2966
|
+
}
|
|
2967
|
+
function extractMcp(event) {
|
|
2968
|
+
const summary = event.summary;
|
|
2969
|
+
if (!isRecord3(summary)) return [];
|
|
2970
|
+
const endpoint = typeof summary.endpoint === "string" ? summary.endpoint : void 0;
|
|
2971
|
+
if (!endpoint) return [];
|
|
2972
|
+
const trust = [];
|
|
2973
|
+
if (Array.isArray(summary.trust)) {
|
|
2974
|
+
for (const t of summary.trust) {
|
|
2975
|
+
if (isRecord3(t) && typeof t.type === "string" && typeof t.detail === "string") {
|
|
2976
|
+
trust.push({ type: t.type, detail: t.detail });
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const si = isRecord3(summary.serverInfo) ? summary.serverInfo : void 0;
|
|
2981
|
+
const name = si && typeof si.name === "string" ? si.name : void 0;
|
|
2982
|
+
return [
|
|
2983
|
+
{
|
|
2984
|
+
id: candidateId(),
|
|
2985
|
+
endpoint,
|
|
2986
|
+
standard: "mcp",
|
|
2987
|
+
origin: event.origin,
|
|
2988
|
+
name,
|
|
2989
|
+
transport: "http",
|
|
2990
|
+
trust,
|
|
2991
|
+
status: event.status,
|
|
2992
|
+
reason: event.reason,
|
|
2993
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2994
|
+
}
|
|
2995
|
+
];
|
|
2996
|
+
}
|
|
2997
|
+
var EXTRACTORS = {
|
|
2998
|
+
ard: extractArd,
|
|
2999
|
+
"ai-catalog": extractAiCatalog,
|
|
3000
|
+
ucp: extractUcp,
|
|
3001
|
+
poppy: extractPoppy,
|
|
3002
|
+
nlweb: extractNlweb,
|
|
3003
|
+
webmcp: extractWebmcp,
|
|
3004
|
+
mcp: extractMcp
|
|
3005
|
+
};
|
|
3006
|
+
function extractCandidates(event) {
|
|
3007
|
+
const extractor = EXTRACTORS[event.standard];
|
|
3008
|
+
if (!extractor) return [];
|
|
3009
|
+
return extractor(event);
|
|
3010
|
+
}
|
|
3011
|
+
function deduplicateCandidates(candidates) {
|
|
3012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3013
|
+
const result = [];
|
|
3014
|
+
for (const c of candidates) {
|
|
3015
|
+
const key = c.endpoint;
|
|
3016
|
+
if (seen.has(key)) continue;
|
|
3017
|
+
seen.add(key);
|
|
3018
|
+
result.push(c);
|
|
3019
|
+
}
|
|
3020
|
+
return result;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/discovery/inbox.ts
|
|
3024
|
+
function inboxPath(stateDir2) {
|
|
3025
|
+
return `${stateDir2}/inbox.json`;
|
|
3026
|
+
}
|
|
3027
|
+
async function readInbox(fs, stateDir2) {
|
|
3028
|
+
try {
|
|
3029
|
+
const data = await fs.readFile(inboxPath(stateDir2));
|
|
3030
|
+
const parsed = JSON.parse(new TextDecoder().decode(data));
|
|
3031
|
+
if (parsed && Array.isArray(parsed.candidates)) return parsed;
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
return { candidates: [] };
|
|
3035
|
+
}
|
|
3036
|
+
async function writeInbox(fs, stateDir2, inbox) {
|
|
3037
|
+
try {
|
|
3038
|
+
await fs.mkdir(stateDir2);
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
const json = JSON.stringify(inbox, null, 2) + "\n";
|
|
3042
|
+
await fs.writeFile(inboxPath(stateDir2), json);
|
|
3043
|
+
}
|
|
3044
|
+
async function appendCandidates(fs, stateDir2, candidates) {
|
|
3045
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3046
|
+
const existing = new Set(inbox.candidates.map((c) => c.endpoint));
|
|
3047
|
+
for (const c of candidates) {
|
|
3048
|
+
if (existing.has(c.endpoint)) continue;
|
|
3049
|
+
existing.add(c.endpoint);
|
|
3050
|
+
inbox.candidates.push(c);
|
|
3051
|
+
}
|
|
3052
|
+
await writeInbox(fs, stateDir2, inbox);
|
|
3053
|
+
}
|
|
3054
|
+
async function listCandidates(fs, stateDir2) {
|
|
3055
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3056
|
+
return inbox.candidates.slice().reverse();
|
|
3057
|
+
}
|
|
3058
|
+
async function getCandidate(fs, stateDir2, id) {
|
|
3059
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3060
|
+
return inbox.candidates.find((c) => c.id === id);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/discovery/probe.ts
|
|
3064
|
+
function isJsonContentType(ct) {
|
|
3065
|
+
if (!ct) return false;
|
|
3066
|
+
const lower = ct.toLowerCase();
|
|
3067
|
+
if (lower.includes("text/html")) return false;
|
|
3068
|
+
return lower.includes("json") || lower.includes("+json");
|
|
3069
|
+
}
|
|
3070
|
+
function parseLinkHeaders(headers) {
|
|
3071
|
+
const links = [];
|
|
3072
|
+
const raw = headers.get("link");
|
|
3073
|
+
if (!raw) return links;
|
|
3074
|
+
const parts = raw.split(/,(?=\s*<)/);
|
|
3075
|
+
for (const part of parts) {
|
|
3076
|
+
const urlMatch = /<([^>]+)>/.exec(part);
|
|
3077
|
+
const relMatch = /rel\s*=\s*"?([^";,\s]+)"?/i.exec(part);
|
|
3078
|
+
if (urlMatch && relMatch) {
|
|
3079
|
+
links.push({ url: urlMatch[1], rel: relMatch[1].toLowerCase() });
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
return links;
|
|
3083
|
+
}
|
|
3084
|
+
function wasRedirected(requestUrl, resp) {
|
|
3085
|
+
if (resp.redirected) return true;
|
|
3086
|
+
if (resp.url && resp.url !== requestUrl) return true;
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
async function probeUrl(fetcher, url, accept) {
|
|
3090
|
+
let resp;
|
|
3091
|
+
try {
|
|
3092
|
+
resp = await fetcher(url, {
|
|
3093
|
+
redirect: "manual",
|
|
3094
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
3095
|
+
credentials: "omit"
|
|
3096
|
+
});
|
|
3097
|
+
} catch {
|
|
3098
|
+
try {
|
|
3099
|
+
resp = await fetcher(url, {
|
|
3100
|
+
redirect: "follow",
|
|
3101
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
3102
|
+
credentials: "omit"
|
|
3103
|
+
});
|
|
3104
|
+
if (wasRedirected(url, resp)) {
|
|
3105
|
+
return { ok: false, status: 301, headers: resp.headers, body: "" };
|
|
3106
|
+
}
|
|
3107
|
+
} catch {
|
|
3108
|
+
return void 0;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (resp.status !== 200)
|
|
3112
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
3113
|
+
const ct = resp.headers.get("content-type");
|
|
3114
|
+
if (!isJsonContentType(ct))
|
|
3115
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
3116
|
+
const body = await resp.text();
|
|
3117
|
+
return { ok: true, status: 200, headers: resp.headers, body };
|
|
3118
|
+
}
|
|
3119
|
+
function isRecord4(v) {
|
|
3120
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3121
|
+
}
|
|
3122
|
+
function makeEvent(standard, origin, url, via, page, status, summary, reason) {
|
|
3123
|
+
return {
|
|
3124
|
+
v: 1,
|
|
3125
|
+
standard,
|
|
3126
|
+
origin,
|
|
3127
|
+
url,
|
|
3128
|
+
via,
|
|
3129
|
+
page,
|
|
3130
|
+
targetId: "",
|
|
3131
|
+
status,
|
|
3132
|
+
summary,
|
|
3133
|
+
...reason ? { reason } : {}
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
function tryParseJson(body) {
|
|
3137
|
+
try {
|
|
3138
|
+
return JSON.parse(body);
|
|
3139
|
+
} catch {
|
|
3140
|
+
return void 0;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function classifyArd(origin, url, via, page, body) {
|
|
3144
|
+
const parsed = tryParseJson(body);
|
|
3145
|
+
if (!isRecord4(parsed))
|
|
3146
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
3147
|
+
if (!Array.isArray(parsed.entries))
|
|
3148
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "missing entries[]");
|
|
3149
|
+
const entries = [];
|
|
3150
|
+
for (const entry of parsed.entries) {
|
|
3151
|
+
if (!isRecord4(entry)) continue;
|
|
3152
|
+
entries.push({
|
|
3153
|
+
identifier: entry.identifier,
|
|
3154
|
+
displayName: entry.displayName,
|
|
3155
|
+
type: entry.type,
|
|
3156
|
+
url: entry.url,
|
|
3157
|
+
capabilities: entry.capabilities,
|
|
3158
|
+
representativeQueries: entry.representativeQueries,
|
|
3159
|
+
trustManifest: entry.trustManifest
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
return makeEvent("ard", origin, url, via, page, "valid", { entries });
|
|
3163
|
+
}
|
|
3164
|
+
function classifyAiCatalog(origin, url, via, page, body) {
|
|
3165
|
+
const parsed = tryParseJson(body);
|
|
3166
|
+
if (!isRecord4(parsed))
|
|
3167
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
3168
|
+
const entries = [];
|
|
3169
|
+
const items = Array.isArray(parsed.entries) ? parsed.entries : Array.isArray(parsed.items) ? parsed.items : void 0;
|
|
3170
|
+
if (!items)
|
|
3171
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "no entries or items");
|
|
3172
|
+
for (const entry of items) {
|
|
3173
|
+
if (!isRecord4(entry)) continue;
|
|
3174
|
+
entries.push({
|
|
3175
|
+
displayName: entry.displayName ?? entry.name,
|
|
3176
|
+
type: entry.type,
|
|
3177
|
+
url: entry.url
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
return makeEvent("ai-catalog", origin, url, via, page, "valid", { entries });
|
|
3181
|
+
}
|
|
3182
|
+
async function probeWellKnown(fetcher, origin, page) {
|
|
3183
|
+
const events = [];
|
|
3184
|
+
const ardUrl = `${origin}/.well-known/ard.json`;
|
|
3185
|
+
const ardResult = await probeUrl(fetcher, ardUrl, "application/json");
|
|
3186
|
+
if (ardResult?.ok) {
|
|
3187
|
+
const ev = classifyArd(origin, ardUrl, "well-known", page, ardResult.body);
|
|
3188
|
+
if (ev) events.push(ev);
|
|
3189
|
+
}
|
|
3190
|
+
const aiCatUrl = `${origin}/.well-known/ai-catalog.json`;
|
|
3191
|
+
const aiCatResult = await probeUrl(fetcher, aiCatUrl, "application/json");
|
|
3192
|
+
if (aiCatResult?.ok) {
|
|
3193
|
+
const ev = classifyAiCatalog(origin, aiCatUrl, "well-known", page, aiCatResult.body);
|
|
3194
|
+
if (ev) events.push(ev);
|
|
3195
|
+
}
|
|
3196
|
+
return events;
|
|
3197
|
+
}
|
|
3198
|
+
async function probeLinkHeaders(fetcher, targetUrl, origin, page) {
|
|
3199
|
+
const events = [];
|
|
3200
|
+
const targetResult = await probeUrl(fetcher, targetUrl);
|
|
3201
|
+
if (!targetResult) return events;
|
|
3202
|
+
const links = parseLinkHeaders(targetResult.headers);
|
|
3203
|
+
for (const link of links) {
|
|
3204
|
+
const resolved = new URL(link.url, targetUrl).href;
|
|
3205
|
+
const classifier = link.rel === "ard" ? classifyArd : link.rel === "ai-catalog" ? classifyAiCatalog : null;
|
|
3206
|
+
if (!classifier) continue;
|
|
3207
|
+
const linkResult = await probeUrl(fetcher, resolved, "application/json");
|
|
3208
|
+
if (linkResult?.ok) {
|
|
3209
|
+
const ev = classifier(origin, resolved, "link-header", page, linkResult.body);
|
|
3210
|
+
if (ev) events.push(ev);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return events;
|
|
3214
|
+
}
|
|
3215
|
+
function mcpJsonRpc(method, params, id = 1) {
|
|
3216
|
+
return JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
3217
|
+
}
|
|
3218
|
+
function parseServerInfo(result) {
|
|
3219
|
+
const si = isRecord4(result.serverInfo) ? result.serverInfo : void 0;
|
|
3220
|
+
if (!si) return void 0;
|
|
3221
|
+
return {
|
|
3222
|
+
name: typeof si.name === "string" ? si.name : void 0,
|
|
3223
|
+
version: typeof si.version === "string" ? si.version : void 0
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function checkAuthResponse(resp) {
|
|
3227
|
+
if (resp.status !== 401) return void 0;
|
|
3228
|
+
const wwwAuth = resp.headers.get("www-authenticate");
|
|
3229
|
+
if (!wwwAuth) return void 0;
|
|
3230
|
+
return { type: "auth-required", detail: wwwAuth.split(",")[0].trim() };
|
|
3231
|
+
}
|
|
3232
|
+
async function tryMcpPost(fetcher, url, body, extraHeaders) {
|
|
3233
|
+
const headers = {
|
|
3234
|
+
"Content-Type": "application/json",
|
|
3235
|
+
Accept: "application/json",
|
|
3236
|
+
...extraHeaders
|
|
3237
|
+
};
|
|
3238
|
+
try {
|
|
3239
|
+
const resp = await fetcher(url, {
|
|
3240
|
+
method: "POST",
|
|
3241
|
+
headers,
|
|
3242
|
+
body,
|
|
3243
|
+
redirect: "manual",
|
|
3244
|
+
credentials: "omit"
|
|
3245
|
+
});
|
|
3246
|
+
const authSignal = checkAuthResponse(resp);
|
|
3247
|
+
if (authSignal) return { url, trust: [authSignal] };
|
|
3248
|
+
if (resp.status === 200) {
|
|
3249
|
+
const text = await resp.text();
|
|
3250
|
+
const parsed = tryParseJson(text);
|
|
3251
|
+
if (isRecord4(parsed) && isRecord4(parsed.result)) {
|
|
3252
|
+
return { url, serverInfo: parseServerInfo(parsed.result), trust: [] };
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
} catch {
|
|
3256
|
+
}
|
|
3257
|
+
return void 0;
|
|
3258
|
+
}
|
|
3259
|
+
async function probeMcpEndpoint(fetcher, url) {
|
|
3260
|
+
const discoverBody = mcpJsonRpc("server/discover", {
|
|
3261
|
+
protocolVersion: "2026-07-28",
|
|
3262
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" }
|
|
3263
|
+
});
|
|
3264
|
+
const hit = await tryMcpPost(fetcher, url, discoverBody, {
|
|
3265
|
+
"Mcp-Method": "server/discover"
|
|
3266
|
+
});
|
|
3267
|
+
if (hit) return hit;
|
|
3268
|
+
const initBody = mcpJsonRpc("initialize", {
|
|
3269
|
+
protocolVersion: "2025-11-25",
|
|
3270
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" },
|
|
3271
|
+
capabilities: {}
|
|
3272
|
+
});
|
|
3273
|
+
return tryMcpPost(fetcher, url, initBody);
|
|
3274
|
+
}
|
|
3275
|
+
async function probeMcp(fetcher, origin, page, targetUrl) {
|
|
3276
|
+
const events = [];
|
|
3277
|
+
const urls = [targetUrl];
|
|
3278
|
+
const mcpUrl = `${origin}/mcp`;
|
|
3279
|
+
if (mcpUrl !== targetUrl) urls.push(mcpUrl);
|
|
3280
|
+
for (const url of urls) {
|
|
3281
|
+
const hit = await probeMcpEndpoint(fetcher, url);
|
|
3282
|
+
if (hit) {
|
|
3283
|
+
const summary = { endpoint: hit.url };
|
|
3284
|
+
if (hit.serverInfo) summary.serverInfo = hit.serverInfo;
|
|
3285
|
+
if (hit.trust.length > 0) summary.trust = hit.trust;
|
|
3286
|
+
events.push(makeEvent("mcp", origin, hit.url, "well-known", page, "valid", summary));
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return events;
|
|
3290
|
+
}
|
|
3291
|
+
async function probeDiscover(platform2, targetUrl) {
|
|
3292
|
+
let origin;
|
|
3293
|
+
try {
|
|
3294
|
+
const u = new URL(targetUrl);
|
|
3295
|
+
origin = u.origin;
|
|
3296
|
+
} catch {
|
|
3297
|
+
return { events: [] };
|
|
3298
|
+
}
|
|
3299
|
+
const page = targetUrl;
|
|
3300
|
+
const fetcher = platform2.fetch;
|
|
3301
|
+
const events = [
|
|
3302
|
+
...await probeWellKnown(fetcher, origin, page),
|
|
3303
|
+
...await probeLinkHeaders(fetcher, targetUrl, origin, page),
|
|
3304
|
+
...await probeMcp(fetcher, origin, page, targetUrl)
|
|
3305
|
+
];
|
|
3306
|
+
return { events };
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// src/discovery/verbs.ts
|
|
3310
|
+
async function readStdin(platform2) {
|
|
3311
|
+
const reader = platform2.stdin.getReader();
|
|
3312
|
+
const chunks = [];
|
|
3313
|
+
for (; ; ) {
|
|
3314
|
+
const { done, value } = await reader.read();
|
|
3315
|
+
if (done) break;
|
|
3316
|
+
chunks.push(value);
|
|
3317
|
+
}
|
|
3318
|
+
reader.releaseLock();
|
|
3319
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
3320
|
+
const merged = new Uint8Array(total);
|
|
3321
|
+
let offset = 0;
|
|
3322
|
+
for (const c of chunks) {
|
|
3323
|
+
merged.set(c, offset);
|
|
3324
|
+
offset += c.length;
|
|
3325
|
+
}
|
|
3326
|
+
return new TextDecoder().decode(merged);
|
|
3327
|
+
}
|
|
3328
|
+
function parseEvents(text) {
|
|
3329
|
+
const lines = text.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
3330
|
+
const events = [];
|
|
3331
|
+
for (const line of lines) {
|
|
3332
|
+
let parsed;
|
|
3333
|
+
try {
|
|
3334
|
+
parsed = JSON.parse(line);
|
|
3335
|
+
} catch {
|
|
3336
|
+
return { error: `invalid JSON: ${line.slice(0, 80)}`, code: 2 };
|
|
3337
|
+
}
|
|
3338
|
+
const ev = parsed;
|
|
3339
|
+
if (typeof ev.v !== "number") return { error: "missing event version", code: 2 };
|
|
3340
|
+
if (ev.v !== 1) return { error: `unknown event version: ${ev.v}`, code: 2 };
|
|
3341
|
+
events.push(ev);
|
|
3342
|
+
}
|
|
3343
|
+
return events;
|
|
3344
|
+
}
|
|
3345
|
+
function formatCandidate(c) {
|
|
3346
|
+
const parts = [`#${c.id} ${c.standard} ${c.endpoint}`];
|
|
3347
|
+
if (c.name) parts[0] += ` (${c.name})`;
|
|
3348
|
+
if (c.status === "invalid") parts.push(` status: invalid \u2013 ${c.reason ?? "unknown"}`);
|
|
3349
|
+
if (c.suggestion) parts.push(` suggestion: ${c.suggestion}`);
|
|
3350
|
+
for (const t of c.trust) {
|
|
3351
|
+
const icon = t.type.includes("mismatch") ? "\u26A0" : "\u2713";
|
|
3352
|
+
parts.push(` ${icon} ${t.type}: ${t.detail}`);
|
|
3353
|
+
}
|
|
3354
|
+
return parts.join("\n");
|
|
3355
|
+
}
|
|
3356
|
+
function formatCandidateJson(c) {
|
|
3357
|
+
return {
|
|
3358
|
+
id: c.id,
|
|
3359
|
+
endpoint: c.endpoint,
|
|
3360
|
+
standard: c.standard,
|
|
3361
|
+
origin: c.origin,
|
|
3362
|
+
name: c.name,
|
|
3363
|
+
transport: c.transport,
|
|
3364
|
+
trust: c.trust,
|
|
3365
|
+
status: c.status,
|
|
3366
|
+
reason: c.reason,
|
|
3367
|
+
suggestion: c.suggestion,
|
|
3368
|
+
discoveredAt: c.discoveredAt
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
async function runFromDiscovery(platform2, args2) {
|
|
3372
|
+
let source = "-";
|
|
3373
|
+
for (const arg of args2) {
|
|
3374
|
+
if (arg !== "--from-discovery" && !arg.startsWith("-")) {
|
|
3375
|
+
source = arg;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
let text;
|
|
3379
|
+
if (source === "-") {
|
|
3380
|
+
text = await readStdin(platform2);
|
|
3381
|
+
} else {
|
|
3382
|
+
try {
|
|
3383
|
+
const data = await platform2.fs.readFile(source);
|
|
3384
|
+
text = new TextDecoder().decode(data);
|
|
3385
|
+
} catch {
|
|
3386
|
+
await platform2.write(2, `mcpcat add --from-discovery: cannot read '${source}'
|
|
3387
|
+
`);
|
|
3388
|
+
return 2;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
const result = parseEvents(text);
|
|
3392
|
+
if ("error" in result) {
|
|
3393
|
+
await platform2.write(2, `mcpcat add --from-discovery: ${result.error}
|
|
3394
|
+
`);
|
|
3395
|
+
return result.code;
|
|
3396
|
+
}
|
|
3397
|
+
resetIdCounter();
|
|
3398
|
+
let allCandidates = [];
|
|
3399
|
+
for (const event of result) {
|
|
3400
|
+
allCandidates.push(...extractCandidates(event));
|
|
3401
|
+
}
|
|
3402
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
3403
|
+
if (allCandidates.length === 0) {
|
|
3404
|
+
await platform2.write(1, "no MCP candidates found\n");
|
|
3405
|
+
return 0;
|
|
3406
|
+
}
|
|
3407
|
+
const sd = stateDir(platform2);
|
|
3408
|
+
await appendCandidates(platform2.fs, sd, allCandidates);
|
|
3409
|
+
for (const c of allCandidates) {
|
|
3410
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3411
|
+
`);
|
|
3412
|
+
}
|
|
3413
|
+
return 0;
|
|
3414
|
+
}
|
|
3415
|
+
async function runInbox(platform2, args2) {
|
|
3416
|
+
const json = args2.includes("--json");
|
|
3417
|
+
const sd = stateDir(platform2);
|
|
3418
|
+
const candidates = await listCandidates(platform2.fs, sd);
|
|
3419
|
+
if (candidates.length === 0) {
|
|
3420
|
+
await platform2.write(1, "inbox is empty\n");
|
|
3421
|
+
return 0;
|
|
3422
|
+
}
|
|
3423
|
+
if (json) {
|
|
3424
|
+
await platform2.write(1, JSON.stringify(candidates.map(formatCandidateJson), null, 2) + "\n");
|
|
3425
|
+
} else {
|
|
3426
|
+
for (const c of candidates) {
|
|
3427
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3428
|
+
`);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return 0;
|
|
3432
|
+
}
|
|
3433
|
+
async function runAddCandidate(platform2, name, candidateId2, configFile) {
|
|
3434
|
+
const sd = stateDir(platform2);
|
|
3435
|
+
const candidate = await getCandidate(platform2.fs, sd, candidateId2);
|
|
3436
|
+
if (!candidate) {
|
|
3437
|
+
await platform2.write(2, `mcpcat add: candidate #${candidateId2} not found in inbox
|
|
3438
|
+
`);
|
|
3439
|
+
return 1;
|
|
3440
|
+
}
|
|
3441
|
+
if (candidate.transport === "webmcp") {
|
|
3442
|
+
await platform2.write(
|
|
3443
|
+
2,
|
|
3444
|
+
`mcpcat add: WebMCP candidates cannot be added directly; use: ${candidate.suggestion}
|
|
3445
|
+
`
|
|
3446
|
+
);
|
|
3447
|
+
return 2;
|
|
3448
|
+
}
|
|
3449
|
+
const path = configFile ?? userConfigPath(platform2);
|
|
3450
|
+
const config = { url: candidate.endpoint };
|
|
3451
|
+
const { replaced } = await addServer(platform2.fs, path, name, config);
|
|
3452
|
+
for (const t of candidate.trust) {
|
|
3453
|
+
if (t.type.includes("mismatch")) {
|
|
3454
|
+
await platform2.write(2, `mcpcat add: warning: ${t.type}: ${t.detail}
|
|
3455
|
+
`);
|
|
2334
3456
|
}
|
|
2335
3457
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
...autoEnableCodemode !== void 0 ? { autoEnableCodemode } : {}
|
|
2340
|
-
};
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
// src/config/paths.ts
|
|
2344
|
-
function userConfigDir(platform2) {
|
|
2345
|
-
const piDir = platform2.env.PI_CODING_AGENT_DIR;
|
|
2346
|
-
if (piDir) return piDir;
|
|
2347
|
-
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "/";
|
|
2348
|
-
return `${home}/.pi/agent`;
|
|
2349
|
-
}
|
|
2350
|
-
function userConfigPath(platform2) {
|
|
2351
|
-
return `${userConfigDir(platform2)}/mcp.json`;
|
|
2352
|
-
}
|
|
2353
|
-
function projectConfigPath(platform2) {
|
|
2354
|
-
return `${platform2.cwd}/.pi/mcp.json`;
|
|
3458
|
+
await platform2.write(1, `${replaced ? "replaced" : "added"} ${name} in ${path}
|
|
3459
|
+
`);
|
|
3460
|
+
return 0;
|
|
2355
3461
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
for (const entry of config.servers) {
|
|
2365
|
-
this.entries.set(entry.name, entry);
|
|
3462
|
+
async function runDiscover(platform2, args2) {
|
|
3463
|
+
const positional = [];
|
|
3464
|
+
let json = false;
|
|
3465
|
+
for (const arg of args2) {
|
|
3466
|
+
if (arg === "--json") {
|
|
3467
|
+
json = true;
|
|
3468
|
+
} else if (!arg.startsWith("-")) {
|
|
3469
|
+
positional.push(arg);
|
|
2366
3470
|
}
|
|
2367
3471
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
2372
|
-
return entry.config;
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
return void 0;
|
|
3472
|
+
if (positional.length < 1) {
|
|
3473
|
+
await platform2.write(2, "mcpcat discover: requires a URL\n");
|
|
3474
|
+
return 2;
|
|
2376
3475
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
3476
|
+
const url = positional[0];
|
|
3477
|
+
if (!URL.canParse(url)) {
|
|
3478
|
+
await platform2.write(2, `mcpcat discover: '${url}' is not a valid URL
|
|
3479
|
+
`);
|
|
3480
|
+
return 2;
|
|
2379
3481
|
}
|
|
2380
|
-
|
|
2381
|
-
|
|
3482
|
+
const { events } = await probeDiscover(platform2, url);
|
|
3483
|
+
resetIdCounter();
|
|
3484
|
+
let allCandidates = [];
|
|
3485
|
+
for (const event of events) {
|
|
3486
|
+
allCandidates.push(...extractCandidates(event));
|
|
2382
3487
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
userPath,
|
|
2388
|
-
projectPath,
|
|
2389
|
-
trustProject: options?.trustProject ?? false,
|
|
2390
|
-
fs: platform2.fs
|
|
2391
|
-
});
|
|
2392
|
-
return new _MergedConfigSource(config);
|
|
3488
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
3489
|
+
if (allCandidates.length > 0) {
|
|
3490
|
+
const sd = stateDir(platform2);
|
|
3491
|
+
await appendCandidates(platform2.fs, sd, allCandidates);
|
|
2393
3492
|
}
|
|
2394
|
-
|
|
3493
|
+
if (allCandidates.length === 0) {
|
|
3494
|
+
await platform2.write(1, "no MCP candidates found\n");
|
|
3495
|
+
return 0;
|
|
3496
|
+
}
|
|
3497
|
+
if (json) {
|
|
3498
|
+
await platform2.write(1, JSON.stringify(allCandidates.map(formatCandidateJson), null, 2) + "\n");
|
|
3499
|
+
} else {
|
|
3500
|
+
for (const c of allCandidates) {
|
|
3501
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3502
|
+
`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return 0;
|
|
3506
|
+
}
|
|
2395
3507
|
|
|
2396
3508
|
// src/config/expand.ts
|
|
2397
3509
|
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
@@ -3786,158 +4898,12 @@ async function handleMcpRequest(request, tools, call, serverName, version, allow
|
|
|
3786
4898
|
};
|
|
3787
4899
|
}
|
|
3788
4900
|
|
|
3789
|
-
// src/config/lock.ts
|
|
3790
|
-
var LOCK_STALE_MS = 1e4;
|
|
3791
|
-
var LOCK_POLL_MS = 50;
|
|
3792
|
-
var LOCK_MAX_WAIT_MS = 5e3;
|
|
3793
|
-
function isEexist(err) {
|
|
3794
|
-
if (err instanceof Error && "code" in err)
|
|
3795
|
-
return err.code === "EEXIST";
|
|
3796
|
-
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
3797
|
-
return false;
|
|
3798
|
-
}
|
|
3799
|
-
async function removeStaleLock(fs, lockPath) {
|
|
3800
|
-
try {
|
|
3801
|
-
const s = await fs.stat(lockPath);
|
|
3802
|
-
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
3803
|
-
try {
|
|
3804
|
-
await fs.rmdir(lockPath);
|
|
3805
|
-
} catch {
|
|
3806
|
-
}
|
|
3807
|
-
return true;
|
|
3808
|
-
}
|
|
3809
|
-
} catch {
|
|
3810
|
-
}
|
|
3811
|
-
return false;
|
|
3812
|
-
}
|
|
3813
|
-
async function acquireLock(fs, configPath) {
|
|
3814
|
-
const lockPath = `${configPath}.lock`;
|
|
3815
|
-
const start = Date.now();
|
|
3816
|
-
while (true) {
|
|
3817
|
-
try {
|
|
3818
|
-
await fs.mkdirExclusive(lockPath);
|
|
3819
|
-
break;
|
|
3820
|
-
} catch (err) {
|
|
3821
|
-
if (!isEexist(err)) throw err;
|
|
3822
|
-
await removeStaleLock(fs, lockPath);
|
|
3823
|
-
}
|
|
3824
|
-
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
3825
|
-
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
3826
|
-
}
|
|
3827
|
-
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
3828
|
-
}
|
|
3829
|
-
let released = false;
|
|
3830
|
-
return async () => {
|
|
3831
|
-
if (released) return;
|
|
3832
|
-
released = true;
|
|
3833
|
-
try {
|
|
3834
|
-
await fs.rmdir(lockPath);
|
|
3835
|
-
} catch {
|
|
3836
|
-
}
|
|
3837
|
-
};
|
|
3838
|
-
}
|
|
3839
|
-
|
|
3840
|
-
// src/config/edit.ts
|
|
3841
|
-
function isRecord2(value) {
|
|
3842
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3843
|
-
}
|
|
3844
|
-
function detectIndent(text) {
|
|
3845
|
-
const match = /^([ \t]+)\S/m.exec(text);
|
|
3846
|
-
return match ? match[1] : " ";
|
|
3847
|
-
}
|
|
3848
|
-
async function readText(fs, path) {
|
|
3849
|
-
try {
|
|
3850
|
-
const data = await fs.readFile(path);
|
|
3851
|
-
return new TextDecoder().decode(data);
|
|
3852
|
-
} catch {
|
|
3853
|
-
return void 0;
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
async function editMcpServers(fs, path, edit) {
|
|
3857
|
-
const release = await acquireLock(fs, path);
|
|
3858
|
-
try {
|
|
3859
|
-
const text = await readText(fs, path);
|
|
3860
|
-
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
3861
|
-
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
3862
|
-
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
3863
|
-
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
3864
|
-
if (!edit(servers, parsed)) return { changed: false };
|
|
3865
|
-
const indent = text ? detectIndent(text) : " ";
|
|
3866
|
-
const output = `${JSON.stringify(parsed, null, indent)}
|
|
3867
|
-
`;
|
|
3868
|
-
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
3869
|
-
if (dir) {
|
|
3870
|
-
try {
|
|
3871
|
-
await fs.mkdir(dir);
|
|
3872
|
-
} catch {
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
3876
|
-
await fs.writeFile(tmpPath, output);
|
|
3877
|
-
await fs.rename(tmpPath, path);
|
|
3878
|
-
return { changed: true };
|
|
3879
|
-
} finally {
|
|
3880
|
-
await release();
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
async function addServer(fs, path, name, config) {
|
|
3884
|
-
let replaced = false;
|
|
3885
|
-
await editMcpServers(fs, path, (servers, parsed) => {
|
|
3886
|
-
const target = servers ?? {};
|
|
3887
|
-
replaced = target[name] !== void 0;
|
|
3888
|
-
target[name] = config;
|
|
3889
|
-
parsed.mcpServers = target;
|
|
3890
|
-
return true;
|
|
3891
|
-
});
|
|
3892
|
-
return { replaced };
|
|
3893
|
-
}
|
|
3894
|
-
async function removeServer(fs, path, name) {
|
|
3895
|
-
let removed = false;
|
|
3896
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3897
|
-
if (!servers || servers[name] === void 0) return false;
|
|
3898
|
-
delete servers[name];
|
|
3899
|
-
removed = true;
|
|
3900
|
-
return true;
|
|
3901
|
-
});
|
|
3902
|
-
return removed;
|
|
3903
|
-
}
|
|
3904
|
-
async function updateServer(fs, path, name, patch) {
|
|
3905
|
-
let updated = false;
|
|
3906
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3907
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3908
|
-
const entry = servers[name];
|
|
3909
|
-
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
3910
|
-
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
3911
|
-
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
3912
|
-
updated = true;
|
|
3913
|
-
return true;
|
|
3914
|
-
});
|
|
3915
|
-
return updated;
|
|
3916
|
-
}
|
|
3917
|
-
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
3918
|
-
let updated = false;
|
|
3919
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3920
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3921
|
-
const entry = servers[name];
|
|
3922
|
-
if (toolGlob) {
|
|
3923
|
-
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
3924
|
-
te[toolGlob] = exposure;
|
|
3925
|
-
entry.toolExposure = te;
|
|
3926
|
-
} else {
|
|
3927
|
-
entry.exposure = exposure;
|
|
3928
|
-
}
|
|
3929
|
-
updated = true;
|
|
3930
|
-
return true;
|
|
3931
|
-
});
|
|
3932
|
-
return updated;
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
4901
|
// src/config/import.ts
|
|
3936
|
-
function
|
|
4902
|
+
function isRecord5(value) {
|
|
3937
4903
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3938
4904
|
}
|
|
3939
4905
|
function isStringRecord2(value) {
|
|
3940
|
-
return
|
|
4906
|
+
return isRecord5(value) && Object.values(value).every((v) => typeof v === "string");
|
|
3941
4907
|
}
|
|
3942
4908
|
function tryParse(text) {
|
|
3943
4909
|
try {
|
|
@@ -3967,17 +4933,17 @@ function importClaude(text, names) {
|
|
|
3967
4933
|
result.errors.push(error);
|
|
3968
4934
|
return result;
|
|
3969
4935
|
}
|
|
3970
|
-
if (!
|
|
4936
|
+
if (!isRecord5(parsed)) {
|
|
3971
4937
|
result.errors.push("expected an object");
|
|
3972
4938
|
return result;
|
|
3973
4939
|
}
|
|
3974
4940
|
const mcpServers = parsed.mcpServers;
|
|
3975
|
-
if (!
|
|
4941
|
+
if (!isRecord5(mcpServers)) {
|
|
3976
4942
|
result.errors.push('no "mcpServers" object');
|
|
3977
4943
|
return result;
|
|
3978
4944
|
}
|
|
3979
4945
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3980
|
-
if (!
|
|
4946
|
+
if (!isRecord5(entry)) {
|
|
3981
4947
|
result.errors.push(`${name}: not an object`);
|
|
3982
4948
|
continue;
|
|
3983
4949
|
}
|
|
@@ -3992,17 +4958,17 @@ function importVSCode(text, names) {
|
|
|
3992
4958
|
result.errors.push(error);
|
|
3993
4959
|
return result;
|
|
3994
4960
|
}
|
|
3995
|
-
if (!
|
|
4961
|
+
if (!isRecord5(parsed)) {
|
|
3996
4962
|
result.errors.push("expected an object");
|
|
3997
4963
|
return result;
|
|
3998
4964
|
}
|
|
3999
4965
|
const servers = parsed.servers ?? parsed.mcpServers;
|
|
4000
|
-
if (!
|
|
4966
|
+
if (!isRecord5(servers)) {
|
|
4001
4967
|
result.errors.push('no "servers" or "mcpServers" object');
|
|
4002
4968
|
return result;
|
|
4003
4969
|
}
|
|
4004
4970
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4005
|
-
if (!
|
|
4971
|
+
if (!isRecord5(entry)) {
|
|
4006
4972
|
result.errors.push(`${name}: not an object`);
|
|
4007
4973
|
continue;
|
|
4008
4974
|
}
|
|
@@ -4036,17 +5002,17 @@ function importSixServers(text, names) {
|
|
|
4036
5002
|
result.errors.push(error);
|
|
4037
5003
|
return result;
|
|
4038
5004
|
}
|
|
4039
|
-
if (!
|
|
5005
|
+
if (!isRecord5(parsed)) {
|
|
4040
5006
|
result.errors.push("expected an object");
|
|
4041
5007
|
return result;
|
|
4042
5008
|
}
|
|
4043
5009
|
const servers = parsed.servers;
|
|
4044
|
-
if (!
|
|
5010
|
+
if (!isRecord5(servers)) {
|
|
4045
5011
|
result.errors.push('no "servers" object');
|
|
4046
5012
|
return result;
|
|
4047
5013
|
}
|
|
4048
5014
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4049
|
-
if (!
|
|
5015
|
+
if (!isRecord5(entry)) {
|
|
4050
5016
|
result.errors.push(`${name}: not an object`);
|
|
4051
5017
|
continue;
|
|
4052
5018
|
}
|
|
@@ -4054,7 +5020,7 @@ function importSixServers(text, names) {
|
|
|
4054
5020
|
if (typeof entry.url === "string") config.url = entry.url;
|
|
4055
5021
|
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
4056
5022
|
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
4057
|
-
if (
|
|
5023
|
+
if (isRecord5(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
4058
5024
|
result.servers.push({ name, config });
|
|
4059
5025
|
}
|
|
4060
5026
|
return result;
|
|
@@ -4071,7 +5037,7 @@ function importCodexToml(text, names) {
|
|
|
4071
5037
|
if (fields.command) config.command = fields.command;
|
|
4072
5038
|
if (fields.args) config.args = fields.args;
|
|
4073
5039
|
if (fields.url) config.url = fields.url;
|
|
4074
|
-
if (fields.env &&
|
|
5040
|
+
if (fields.env && isRecord5(fields.env)) config.env = fields.env;
|
|
4075
5041
|
result.servers.push({ name, config });
|
|
4076
5042
|
}
|
|
4077
5043
|
return result;
|
|
@@ -4123,7 +5089,7 @@ function convertOpenCodeLocal(entry) {
|
|
|
4123
5089
|
} else if (typeof entry.command === "string") {
|
|
4124
5090
|
config.command = entry.command;
|
|
4125
5091
|
}
|
|
4126
|
-
if (
|
|
5092
|
+
if (isRecord5(entry.environment)) {
|
|
4127
5093
|
const env = {};
|
|
4128
5094
|
for (const [k, v] of Object.entries(entry.environment)) {
|
|
4129
5095
|
if (typeof v === "string")
|
|
@@ -4146,17 +5112,17 @@ function importOpenCode(text, names) {
|
|
|
4146
5112
|
result.errors.push(error);
|
|
4147
5113
|
return result;
|
|
4148
5114
|
}
|
|
4149
|
-
if (!
|
|
5115
|
+
if (!isRecord5(parsed)) {
|
|
4150
5116
|
result.errors.push("expected an object");
|
|
4151
5117
|
return result;
|
|
4152
5118
|
}
|
|
4153
5119
|
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
4154
|
-
if (!
|
|
5120
|
+
if (!isRecord5(mcpServers)) {
|
|
4155
5121
|
result.errors.push("no mcp servers found");
|
|
4156
5122
|
return result;
|
|
4157
5123
|
}
|
|
4158
5124
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
4159
|
-
if (!
|
|
5125
|
+
if (!isRecord5(entry)) {
|
|
4160
5126
|
result.errors.push(`${name}: not an object`);
|
|
4161
5127
|
continue;
|
|
4162
5128
|
}
|
|
@@ -4172,9 +5138,9 @@ function detectFormat(text, filename) {
|
|
|
4172
5138
|
if (filename.includes(".vscode")) return "vscode";
|
|
4173
5139
|
try {
|
|
4174
5140
|
const parsed = JSON.parse(text);
|
|
4175
|
-
if (
|
|
4176
|
-
if (parsed.servers &&
|
|
4177
|
-
if (parsed.servers &&
|
|
5141
|
+
if (isRecord5(parsed)) {
|
|
5142
|
+
if (parsed.servers && isRecord5(parsed.servers) && parsed.version !== void 0) return "six";
|
|
5143
|
+
if (parsed.servers && isRecord5(parsed.servers)) return "vscode";
|
|
4178
5144
|
if (parsed.mcpServers) return "claude";
|
|
4179
5145
|
if (parsed.mcp) return "opencode";
|
|
4180
5146
|
}
|
|
@@ -4743,9 +5709,7 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
4743
5709
|
"task",
|
|
4744
5710
|
"login",
|
|
4745
5711
|
"logout",
|
|
4746
|
-
"code"
|
|
4747
|
-
"discover",
|
|
4748
|
-
"inbox"
|
|
5712
|
+
"code"
|
|
4749
5713
|
]);
|
|
4750
5714
|
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
4751
5715
|
function helpText(version) {
|
|
@@ -4773,8 +5737,8 @@ Configuration verbs:
|
|
|
4773
5737
|
Server and code verbs:
|
|
4774
5738
|
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4775
5739
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
4776
|
-
discover <url> Find MCP servers
|
|
4777
|
-
inbox
|
|
5740
|
+
discover <url> Find MCP servers at a URL
|
|
5741
|
+
inbox [--json] List discovery candidates
|
|
4778
5742
|
help [verb] Show help
|
|
4779
5743
|
|
|
4780
5744
|
Options:
|
|
@@ -4905,6 +5869,21 @@ function renderCallResult(result) {
|
|
|
4905
5869
|
}
|
|
4906
5870
|
return parts.join("\n");
|
|
4907
5871
|
}
|
|
5872
|
+
var NETWORK_HINT = "connection failed (use slicc-node, slicc-swift, or the browser extension for servers without CORS)";
|
|
5873
|
+
function transportHint(err) {
|
|
5874
|
+
if (err instanceof TypeError) {
|
|
5875
|
+
const cause = err.cause;
|
|
5876
|
+
if (cause?.code === "ECONNREFUSED" || cause?.code === "ENETUNREACH") {
|
|
5877
|
+
return `connection failed: ${err.message}`;
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
if (err instanceof Error && "status" in err && err.status === 502) {
|
|
5881
|
+
const msg = err.message;
|
|
5882
|
+
if (msg) return `${NETWORK_HINT}: ${msg}`;
|
|
5883
|
+
return NETWORK_HINT;
|
|
5884
|
+
}
|
|
5885
|
+
return void 0;
|
|
5886
|
+
}
|
|
4908
5887
|
async function readDataArg(platform2, dataArg) {
|
|
4909
5888
|
let raw;
|
|
4910
5889
|
if (dataArg.startsWith("@-")) {
|
|
@@ -5074,6 +6053,37 @@ async function dispatchConfigVerb(platform2, verb, args2) {
|
|
|
5074
6053
|
return 1;
|
|
5075
6054
|
}
|
|
5076
6055
|
}
|
|
6056
|
+
async function dispatchDiscoveryVerb(platform2, parsed, args2) {
|
|
6057
|
+
if (parsed.verb === "add" && args2.includes("--from-discovery")) {
|
|
6058
|
+
return runFromDiscovery(platform2, args2.slice(1));
|
|
6059
|
+
}
|
|
6060
|
+
if (parsed.verb === "add" && args2.includes("--candidate")) {
|
|
6061
|
+
const rest = args2.slice(1);
|
|
6062
|
+
const nameIdx = rest.findIndex((a) => !a.startsWith("-") && a !== "--candidate");
|
|
6063
|
+
const candidateIdx = rest.indexOf("--candidate");
|
|
6064
|
+
if (nameIdx < 0 || candidateIdx < 0 || candidateIdx + 1 >= rest.length) {
|
|
6065
|
+
await platform2.write(2, "mcpcat add --candidate: requires <name> --candidate <id>\n");
|
|
6066
|
+
return 2;
|
|
6067
|
+
}
|
|
6068
|
+
const name = rest[nameIdx];
|
|
6069
|
+
const candidateId2 = rest[candidateIdx + 1];
|
|
6070
|
+
const configIdx = rest.indexOf("--config");
|
|
6071
|
+
const configFile = configIdx >= 0 && configIdx + 1 < rest.length ? rest[configIdx + 1] : void 0;
|
|
6072
|
+
return runAddCandidate(platform2, name, candidateId2, configFile);
|
|
6073
|
+
}
|
|
6074
|
+
if (parsed.verb === "discover") return runDiscover(platform2, args2.slice(1));
|
|
6075
|
+
if (parsed.verb === "inbox") return runInbox(platform2, args2.slice(1));
|
|
6076
|
+
return void 0;
|
|
6077
|
+
}
|
|
6078
|
+
async function dispatchServeVerb(platform2, args2, version) {
|
|
6079
|
+
const serveArgs = args2.slice(args2.indexOf("serve") + 1);
|
|
6080
|
+
const serveOptions = parseServeFlags(serveArgs);
|
|
6081
|
+
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
6082
|
+
await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
6083
|
+
return 2;
|
|
6084
|
+
}
|
|
6085
|
+
return runServe(platform2, serveOptions, version);
|
|
6086
|
+
}
|
|
5077
6087
|
async function run(platform2, version, args2) {
|
|
5078
6088
|
if (args2.includes("--version") || args2.includes("-V")) {
|
|
5079
6089
|
await platform2.write(1, `${version}
|
|
@@ -5094,21 +6104,19 @@ async function run(platform2, version, args2) {
|
|
|
5094
6104
|
return 2;
|
|
5095
6105
|
}
|
|
5096
6106
|
if (parsed.verb in CONFIG_VERBS) {
|
|
6107
|
+
const disc = await dispatchDiscoveryVerb(platform2, parsed, args2);
|
|
6108
|
+
if (disc !== void 0) return disc;
|
|
5097
6109
|
return dispatchConfigVerb(platform2, parsed.verb, args2);
|
|
5098
6110
|
}
|
|
6111
|
+
const discResult = await dispatchDiscoveryVerb(platform2, parsed, args2);
|
|
6112
|
+
if (discResult !== void 0) return discResult;
|
|
5099
6113
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
5100
6114
|
await platform2.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
5101
6115
|
`);
|
|
5102
6116
|
return 1;
|
|
5103
6117
|
}
|
|
5104
6118
|
if (parsed.verb === "serve") {
|
|
5105
|
-
|
|
5106
|
-
const serveOptions = parseServeFlags(serveArgs);
|
|
5107
|
-
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
5108
|
-
await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
5109
|
-
return 2;
|
|
5110
|
-
}
|
|
5111
|
-
return runServe(platform2, serveOptions, version);
|
|
6119
|
+
return dispatchServeVerb(platform2, args2, version);
|
|
5112
6120
|
}
|
|
5113
6121
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
5114
6122
|
await platform2.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
@@ -5146,7 +6154,8 @@ async function run(platform2, version, args2) {
|
|
|
5146
6154
|
version
|
|
5147
6155
|
});
|
|
5148
6156
|
} catch (err) {
|
|
5149
|
-
|
|
6157
|
+
const msg = transportHint(err) ?? `connection failed: ${err.message}`;
|
|
6158
|
+
await platform2.write(2, `mcpcat: ${msg}
|
|
5150
6159
|
`);
|
|
5151
6160
|
return 3;
|
|
5152
6161
|
}
|
|
@@ -5176,11 +6185,13 @@ import { access, mkdir, readFile as readFile2, rename, rmdir, stat, writeFile }
|
|
|
5176
6185
|
import { createServer } from "node:net";
|
|
5177
6186
|
import { Readable, Writable } from "node:stream";
|
|
5178
6187
|
import * as tty from "node:tty";
|
|
6188
|
+
var USE_PROCESS_GROUPS2 = process.platform !== "win32";
|
|
5179
6189
|
function nodeSpawn(options) {
|
|
5180
6190
|
const child = cpSpawn(options.argv[0], options.argv.slice(1), {
|
|
5181
6191
|
env: options.env ?? process.env,
|
|
5182
6192
|
cwd: options.cwd,
|
|
5183
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
6193
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6194
|
+
detached: USE_PROCESS_GROUPS2
|
|
5184
6195
|
});
|
|
5185
6196
|
const stdout = Readable.toWeb(child.stdout);
|
|
5186
6197
|
const stderr = Readable.toWeb(child.stderr);
|
|
@@ -5200,7 +6211,19 @@ function nodeSpawn(options) {
|
|
|
5200
6211
|
});
|
|
5201
6212
|
},
|
|
5202
6213
|
kill(signal) {
|
|
5203
|
-
|
|
6214
|
+
const sig = signal ?? "SIGTERM";
|
|
6215
|
+
const pid = child.pid;
|
|
6216
|
+
if (USE_PROCESS_GROUPS2 && pid !== void 0) {
|
|
6217
|
+
try {
|
|
6218
|
+
process.kill(-pid, sig);
|
|
6219
|
+
return;
|
|
6220
|
+
} catch {
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
try {
|
|
6224
|
+
child.kill(sig);
|
|
6225
|
+
} catch {
|
|
6226
|
+
}
|
|
5204
6227
|
}
|
|
5205
6228
|
});
|
|
5206
6229
|
}
|
|
@@ -5354,5 +6377,5 @@ function createHostPlatform() {
|
|
|
5354
6377
|
// src/host.ts
|
|
5355
6378
|
var platform = createHostPlatform();
|
|
5356
6379
|
var args = process.argv.slice(2);
|
|
5357
|
-
var code = await run(platform, "0.
|
|
6380
|
+
var code = await run(platform, "0.6.0", args);
|
|
5358
6381
|
process.exit(code);
|