@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.
@@ -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
- transport = new StdioTransport({
1726
- command: options.command,
1727
- args: options.args,
1728
- env: options.env,
1729
- cwd: options.cwd,
1730
- inheritEnv: true,
1731
- stderr: "pipe"
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 nextId = 1;
2011
+ let nextId2 = 1;
1821
2012
  let negotiatedVersion;
1822
2013
  async function request(method, params) {
1823
- const id = nextId++;
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
- return {
2337
- servers: [...servers.values()],
2338
- problems,
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
- // src/config/index.ts
2358
- var MergedConfigSource = class _MergedConfigSource {
2359
- entries;
2360
- config;
2361
- constructor(config) {
2362
- this.config = config;
2363
- this.entries = /* @__PURE__ */ new Map();
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
- async get(name) {
2369
- const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
2370
- for (const [key, entry] of this.entries) {
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
- entry(name) {
2378
- return this.entries.get(name);
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
- list() {
2381
- return [...this.entries.values()];
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
- static async load(platform2, options) {
2384
- const userPath = options?.configFile ?? userConfigPath(platform2);
2385
- const projectPath = options?.configFile ? void 0 : projectConfigPath(platform2);
2386
- const config = await loadConfig({
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 isRecord3(value) {
4902
+ function isRecord5(value) {
3937
4903
  return typeof value === "object" && value !== null && !Array.isArray(value);
3938
4904
  }
3939
4905
  function isStringRecord2(value) {
3940
- return isRecord3(value) && Object.values(value).every((v) => typeof v === "string");
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 (!isRecord3(parsed)) {
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 (!isRecord3(mcpServers)) {
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 (!isRecord3(entry)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(servers)) {
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 (!isRecord3(entry)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(servers)) {
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 (!isRecord3(entry)) {
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 (isRecord3(entry.toolExposure)) config.toolExposure = entry.toolExposure;
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 && isRecord3(fields.env)) config.env = 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 (isRecord3(entry.environment)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(mcpServers)) {
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 (!isRecord3(entry)) {
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 (isRecord3(parsed)) {
4176
- if (parsed.servers && isRecord3(parsed.servers) && parsed.version !== void 0) return "six";
4177
- if (parsed.servers && isRecord3(parsed.servers)) return "vscode";
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 (not implemented yet)
4777
- inbox Discovery inbox (not implemented yet)
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
- const serveArgs = args2.slice(args2.indexOf("serve") + 1);
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
- await platform2.write(2, `mcpcat: connection failed: ${err.message}
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
- child.kill(signal ?? "SIGTERM");
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.4.0", args);
6380
+ var code = await run(platform, "0.6.0", args);
5358
6381
  process.exit(code);