@ai-ecoverse/mcpcat 0.20.2 → 0.20.3

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.
@@ -8485,8 +8485,19 @@ function buildServerInfo(discoverResult2) {
8485
8485
  instructions: discoverResult2?.instructions
8486
8486
  };
8487
8487
  }
8488
+ function readHiddenTools(result) {
8489
+ const meta = result?._meta;
8490
+ const hidden = meta?.["io.mcpcat/hidden"];
8491
+ const out = {};
8492
+ if (!hidden || typeof hidden !== "object" || Array.isArray(hidden)) return out;
8493
+ for (const [name, why] of Object.entries(hidden)) {
8494
+ if (typeof why === "string") out[name] = why;
8495
+ }
8496
+ return out;
8497
+ }
8488
8498
  function buildStatelessMethods(requestFn, discoverResult2) {
8489
8499
  let serverInfoCache;
8500
+ let hidden = {};
8490
8501
  return {
8491
8502
  async info() {
8492
8503
  if (serverInfoCache) return serverInfoCache;
@@ -8494,7 +8505,12 @@ function buildStatelessMethods(requestFn, discoverResult2) {
8494
8505
  return serverInfoCache;
8495
8506
  },
8496
8507
  async listTools() {
8497
- return (await requestFn("tools/list", {}))?.tools ?? [];
8508
+ const result = await requestFn("tools/list", {});
8509
+ hidden = readHiddenTools(result);
8510
+ return result?.tools ?? [];
8511
+ },
8512
+ hiddenTool(name) {
8513
+ return Object.hasOwn(hidden, name) ? hidden[name] : void 0;
8498
8514
  },
8499
8515
  async callTool(name, args2, _callOptions) {
8500
8516
  const params = {
@@ -8934,12 +8950,18 @@ function buildServerInfo2(discoverResult2) {
8934
8950
  };
8935
8951
  }
8936
8952
  function buildConnectionMethods(rpc, discoverResult2, proc) {
8953
+ let hidden = {};
8937
8954
  return {
8938
8955
  async info() {
8939
8956
  return buildServerInfo2(discoverResult2);
8940
8957
  },
8941
8958
  async listTools() {
8942
- return (await rpc.request("tools/list", {}))?.tools ?? [];
8959
+ const result = await rpc.request("tools/list", {});
8960
+ hidden = readHiddenTools(result);
8961
+ return result?.tools ?? [];
8962
+ },
8963
+ hiddenTool(name) {
8964
+ return Object.hasOwn(hidden, name) ? hidden[name] : void 0;
8943
8965
  },
8944
8966
  async callTool(name, args2, callOptions) {
8945
8967
  const params = { name, arguments: args2 ?? {} };
@@ -9141,7 +9163,7 @@ function readLine(platform2) {
9141
9163
  return new Promise((resolve, reject) => {
9142
9164
  const reader = platform2.stdin.getReader();
9143
9165
  const chunks = [];
9144
- function pump() {
9166
+ function pump2() {
9145
9167
  reader.read().then(({ done, value }) => {
9146
9168
  if (done) {
9147
9169
  reader.releaseLock();
@@ -9155,10 +9177,10 @@ function readLine(platform2) {
9155
9177
  resolve(text.split("\n")[0]);
9156
9178
  return;
9157
9179
  }
9158
- pump();
9180
+ pump2();
9159
9181
  }, reject);
9160
9182
  }
9161
- pump();
9183
+ pump2();
9162
9184
  });
9163
9185
  }
9164
9186
  function concatChunks(chunks) {
@@ -12037,7 +12059,7 @@ async function writeResponse(writer, response) {
12037
12059
  var PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-06-18"];
12038
12060
  var OUTPUT_CAP = 1048576;
12039
12061
  var JSON_CT = "application/json; charset=utf-8";
12040
- async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
12062
+ async function handleJsonRpc(raw, source, call, serverName, serverVersion) {
12041
12063
  let parsed;
12042
12064
  try {
12043
12065
  parsed = JSON.parse(raw);
@@ -12054,16 +12076,26 @@ async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
12054
12076
  if (method === "initialize")
12055
12077
  return rpcOk(id, initializeResult(params, serverName, serverVersion));
12056
12078
  if (method === "server/discover") return rpcOk(id, discoverResult(serverName, serverVersion));
12057
- if (method === "tools/list") return rpcOk(id, { tools });
12058
12079
  if (method === "ping") return rpcOk(id, {});
12059
- if (method === "tools/call") return toolCall(id, params, tools, call);
12080
+ if (method === "tools/list" || method === "tools/call") {
12081
+ const listing = Array.isArray(source) ? { tools: source } : await source();
12082
+ if (listing.error) return rpcError(id, -32e3, listing.error);
12083
+ if (method === "tools/call") return toolCall(id, params, listing, call);
12084
+ const hidden = listing.hidden && Object.keys(listing.hidden).length > 0 ? listing.hidden : null;
12085
+ return rpcOk(id, {
12086
+ tools: listing.tools,
12087
+ ...hidden ? { _meta: { "io.mcpcat/hidden": hidden } } : {}
12088
+ });
12089
+ }
12060
12090
  return rpcError(id, -32601, `Method not found: ${method}`);
12061
12091
  }
12062
- async function toolCall(id, params, tools, call) {
12092
+ async function toolCall(id, params, { tools, hidden }, call) {
12063
12093
  const name = params && typeof params === "object" ? readString(params, "name") : void 0;
12064
12094
  if (!name) return rpcError(id, -32602, "tools/call requires a name");
12065
- if (!tools.some((tool) => tool.name === name))
12066
- return rpcError(id, -32602, `Unknown tool: ${name}`);
12095
+ if (!tools.some((tool) => tool.name === name)) {
12096
+ const why = hidden && Object.hasOwn(hidden, name) ? hidden[name] : void 0;
12097
+ return rpcError(id, -32602, why ? `Unknown tool: ${name}: ${why}` : `Unknown tool: ${name}`);
12098
+ }
12067
12099
  const args2 = params && typeof params === "object" ? Object.getOwnPropertyDescriptor(params, "arguments")?.value : void 0;
12068
12100
  try {
12069
12101
  const result = await call(name, args2);
@@ -12427,7 +12459,149 @@ ${tail}`,
12427
12459
  }
12428
12460
  };
12429
12461
  }
12430
- async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12462
+ var DIALOG_POLL_MS = 250;
12463
+ var DIALOG_READY_MS = 1e3;
12464
+ var DIALOG_STALL_MS = 1e3;
12465
+ function watchLoop(watch) {
12466
+ let timer;
12467
+ const result = new Promise((resolve) => {
12468
+ timer = setInterval(() => {
12469
+ const found = watch();
12470
+ if (found) {
12471
+ clearInterval(timer);
12472
+ resolve(found);
12473
+ }
12474
+ }, DIALOG_POLL_MS);
12475
+ });
12476
+ return {
12477
+ result,
12478
+ stop() {
12479
+ clearInterval(timer);
12480
+ }
12481
+ };
12482
+ }
12483
+ async function browserSocketUrl(platform2, cdp) {
12484
+ if (/^wss?:\/\//i.test(cdp)) return cdp;
12485
+ if (!/^https?:\/\//i.test(cdp)) return void 0;
12486
+ const res = await platform2.fetch(`${cdp.replace(/\/+$/, "")}/json/version`, {
12487
+ signal: AbortSignal.timeout(DIALOG_READY_MS)
12488
+ });
12489
+ const body = await res.json();
12490
+ return typeof body.webSocketDebuggerUrl === "string" ? body.webSocketDebuggerUrl : void 0;
12491
+ }
12492
+ function parseCdp(data) {
12493
+ try {
12494
+ return JSON.parse(
12495
+ typeof data === "string" ? data : new TextDecoder().decode(data)
12496
+ );
12497
+ } catch {
12498
+ return void 0;
12499
+ }
12500
+ }
12501
+ function applyCdp(state, channel, send, msg, settle) {
12502
+ if (msg.method === "Page.javascriptDialogOpening") {
12503
+ state.open = typeof msg.params?.type === "string" ? msg.params.type : "dialog";
12504
+ } else if (msg.method === "Page.javascriptDialogClosed") {
12505
+ state.open = void 0;
12506
+ } else if (msg.method === "Target.detachedFromTarget") {
12507
+ channel.close();
12508
+ } else if (msg.id === 1) {
12509
+ const sessionId = msg.result?.sessionId;
12510
+ if (typeof sessionId !== "string") {
12511
+ channel.close();
12512
+ return;
12513
+ }
12514
+ state.enableSentAt = Date.now();
12515
+ send({ id: 2, sessionId, method: "Page.enable" });
12516
+ } else if (msg.id === 2) {
12517
+ state.enableSentAt = void 0;
12518
+ settle();
12519
+ }
12520
+ }
12521
+ async function pump(state, channel, send, settle) {
12522
+ const reader = channel.readable.getReader();
12523
+ try {
12524
+ for (; ; ) {
12525
+ const { done, value } = await reader.read();
12526
+ if (done) return;
12527
+ const msg = parseCdp(value);
12528
+ if (msg) applyCdp(state, channel, send, msg, settle);
12529
+ }
12530
+ } catch {
12531
+ return;
12532
+ }
12533
+ }
12534
+ async function attachSocket(platform2, state, url, targetId) {
12535
+ const channel = await platform2.websocket(url).catch(() => void 0);
12536
+ if (!channel) return;
12537
+ state.channel = channel;
12538
+ const writer = channel.writable.getWriter();
12539
+ const send = (message) => {
12540
+ writer.write(JSON.stringify(message)).catch(() => channel.close());
12541
+ };
12542
+ let settle;
12543
+ const settled = new Promise((resolve) => {
12544
+ settle = resolve;
12545
+ });
12546
+ void pump(state, channel, send, settle).then(() => {
12547
+ if (state.channel === channel) {
12548
+ state.channel = void 0;
12549
+ state.open = void 0;
12550
+ state.enableSentAt = void 0;
12551
+ }
12552
+ channel.close();
12553
+ settle();
12554
+ });
12555
+ send({ id: 1, method: "Target.attachToTarget", params: { targetId, flatten: true } });
12556
+ await settled;
12557
+ }
12558
+ function watchJsDialogs(platform2, cdp, targetId) {
12559
+ const state = {};
12560
+ let connecting;
12561
+ let closed = false;
12562
+ const connect2 = async () => {
12563
+ if (!platform2.websocket) return;
12564
+ const url = await browserSocketUrl(platform2, cdp).catch(() => void 0);
12565
+ if (!url || closed) return;
12566
+ await attachSocket(platform2, state, url, targetId);
12567
+ };
12568
+ return {
12569
+ async ready() {
12570
+ if (closed || state.channel) return;
12571
+ connecting ??= connect2().finally(() => {
12572
+ connecting = void 0;
12573
+ });
12574
+ let timer;
12575
+ await Promise.race([
12576
+ connecting,
12577
+ new Promise((resolve) => {
12578
+ timer = setTimeout(resolve, DIALOG_READY_MS);
12579
+ })
12580
+ ]);
12581
+ clearTimeout(timer);
12582
+ },
12583
+ dialog: () => state.open,
12584
+ stalled: () => state.enableSentAt !== void 0 && Date.now() - state.enableSentAt >= DIALOG_STALL_MS,
12585
+ close() {
12586
+ closed = true;
12587
+ state.channel?.close();
12588
+ }
12589
+ };
12590
+ }
12591
+ function dialogMessage(tab, type) {
12592
+ return `tab ${tab} is blocked by a JavaScript ${type.replace(/[^a-z]/gi, "")} dialog; answer or dismiss it in the browser, then retry`;
12593
+ }
12594
+ function timeoutMessage(verb, timeoutMs, tab, watcher) {
12595
+ const head = `${verb} timed out after ${Math.round(timeoutMs / 1e3)}s`;
12596
+ if (!watcher) {
12597
+ return `${head}; if the page shows a JavaScript dialog (alert, confirm, prompt), answer it, and pass --cdp <url> so mcpcat can detect one`;
12598
+ }
12599
+ if (watcher.stalled()) {
12600
+ return `${head}; tab ${tab} does not answer the DevTools protocol, so a JavaScript dialog opened before mcpcat attached may block it; answer it in the browser, then retry`;
12601
+ }
12602
+ return head;
12603
+ }
12604
+ async function runProcess(platform2, argv, stdin, timeoutMs, limit, watch) {
12431
12605
  let proc;
12432
12606
  try {
12433
12607
  proc = await platform2.spawn({ argv });
@@ -12471,12 +12645,20 @@ async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12471
12645
  readStream3(proc.stderr, err),
12472
12646
  proc.wait()
12473
12647
  ]);
12474
- const result = await Promise.race([work, timer]);
12648
+ const watcher = watch ? watchLoop(watch) : void 0;
12649
+ const blocked = watcher ? watcher.result.then((type) => ({ blocked: type })) : new Promise(() => {
12650
+ });
12651
+ const result = await Promise.race([work, timer, blocked]);
12475
12652
  clearTimeout(timeoutId);
12653
+ watcher?.stop();
12476
12654
  if (result === "timeout") {
12477
12655
  await proc.kill("SIGTERM");
12478
12656
  return { stdout: "", stderr: "timed out", exitCode: 124, cut: false };
12479
12657
  }
12658
+ if (!Array.isArray(result)) {
12659
+ await proc.kill("SIGTERM");
12660
+ return { stdout: "", stderr: "blocked", exitCode: 124, cut: false, blocked: result.blocked };
12661
+ }
12480
12662
  const [, , exitResult] = result;
12481
12663
  const stdout = out.finish();
12482
12664
  const stderr = err.finish();
@@ -12487,7 +12669,7 @@ async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12487
12669
  cut: stdout.cut
12488
12670
  };
12489
12671
  }
12490
- async function resolveWebMcpTab(platform2, options) {
12672
+ async function listTabs(platform2, options) {
12491
12673
  const argv = [options.playwrightCli, "tab-list"];
12492
12674
  if (options.cdp) argv.push("--cdp", options.cdp);
12493
12675
  const result = await runProcess(
@@ -12514,6 +12696,13 @@ async function resolveWebMcpTab(platform2, options) {
12514
12696
  if (new Set(rows.map((r) => r.id)).size !== rows.length || active.length > 1) {
12515
12697
  return { error: "could not read playwright-cli tab-list" };
12516
12698
  }
12699
+ return { rows };
12700
+ }
12701
+ async function resolveWebMcpTab(platform2, options) {
12702
+ const listed = await listTabs(platform2, options);
12703
+ if ("error" in listed) return listed;
12704
+ const { rows } = listed;
12705
+ const active = rows.filter((r) => r.active);
12517
12706
  if (active.length === 1) return { tab: active[0].id };
12518
12707
  if (rows.length === 1) return { tab: rows[0].id };
12519
12708
  return {
@@ -12576,20 +12765,57 @@ function ambiguityError(name, peers) {
12576
12765
  `tool "${name}" is ambiguous: ${peers.length} tools on the page match it (${origins}), and playwright-cli runs the first tool by name and cannot select one by origin, so mcpcat refuses the call`
12577
12766
  );
12578
12767
  }
12768
+ async function blockedNow(watcher, tab) {
12769
+ if (!watcher) return void 0;
12770
+ await watcher.ready();
12771
+ const open2 = watcher.dialog();
12772
+ return open2 ? dialogMessage(tab, open2) : void 0;
12773
+ }
12774
+ async function tabGone(platform2, options) {
12775
+ const { tab } = options;
12776
+ if (!tab) return void 0;
12777
+ const listed = await listTabs(platform2, options);
12778
+ if ("error" in listed || listed.rows.some((r) => r.id === tab)) return void 0;
12779
+ return `tab ${tab} is gone (the browser restarted or the tab closed); restart mcpcat serve --webmcp, or pass --tab <targetId> (see playwright-cli tab-list)`;
12780
+ }
12781
+ function hiddenToolMessage(name) {
12782
+ return `tool ${name} exists on the page but is hidden because it is consequential (consequentialHint); restart with mcpcat serve --webmcp --allow-consequential to expose it`;
12783
+ }
12579
12784
  function createWebMcpHandler(platform2, options) {
12580
12785
  let cache;
12581
12786
  const baseArgs = buildBaseArgs(options);
12582
12787
  const listLimit = Math.max(options.maxOutput, WEBMCP_LIST_FLOOR);
12788
+ const { cdp, tab } = options;
12789
+ const watcher = cdp && tab ? watchJsDialogs(platform2, cdp, tab) : void 0;
12790
+ const watch = watcher ? () => watcher.dialog() : void 0;
12583
12791
  async function listRaw(fresh = false) {
12792
+ const blocked = await blockedNow(watcher, tab);
12793
+ if (blocked) return { tools: [], error: blocked };
12584
12794
  const now = Date.now();
12585
12795
  if (!fresh && cache && now < cache.expiresAt) {
12586
12796
  return { tools: cache.tools };
12587
12797
  }
12588
12798
  const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
12589
- const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs, listLimit);
12799
+ const result = await runProcess(
12800
+ platform2,
12801
+ argv,
12802
+ void 0,
12803
+ options.callTimeoutMs,
12804
+ listLimit,
12805
+ watch
12806
+ );
12590
12807
  if (result.exitCode === -1) {
12591
12808
  return { tools: [], error: result.stderr, notFound: true };
12592
12809
  }
12810
+ if (result.blocked) {
12811
+ return { tools: [], error: dialogMessage(tab, result.blocked) };
12812
+ }
12813
+ if (result.exitCode === 124) {
12814
+ return {
12815
+ tools: [],
12816
+ error: timeoutMessage("webmcp-list", options.callTimeoutMs, tab, watcher)
12817
+ };
12818
+ }
12593
12819
  if (result.exitCode !== 0) {
12594
12820
  return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
12595
12821
  }
@@ -12605,6 +12831,10 @@ function createWebMcpHandler(platform2, options) {
12605
12831
  if (!Array.isArray(parsed)) {
12606
12832
  return { tools: [], error: "webmcp-list did not return an array" };
12607
12833
  }
12834
+ if (parsed.length === 0) {
12835
+ const gone2 = await tabGone(platform2, options);
12836
+ if (gone2) return { tools: [], error: gone2 };
12837
+ }
12608
12838
  const tools = parsed;
12609
12839
  cache = { tools, expiresAt: now + options.ttlMs };
12610
12840
  return { tools };
@@ -12612,11 +12842,13 @@ function createWebMcpHandler(platform2, options) {
12612
12842
  async function getRpcTools() {
12613
12843
  const { tools, error, notFound } = await listRaw();
12614
12844
  if (notFound) return null;
12615
- if (error) return [];
12845
+ if (error) return { tools: [], error };
12616
12846
  const rpcTools = [];
12847
+ const hidden = {};
12617
12848
  for (const { name, tool, peers } of exposedTools(tools)) {
12618
12849
  if (peers.length > 1) continue;
12619
12850
  if (tool.annotations?.consequentialHint && !options.allowConsequential) {
12851
+ hidden[name] = hiddenToolMessage(name);
12620
12852
  continue;
12621
12853
  }
12622
12854
  const mapped = mapDescriptionAnnotations(tool.annotations);
@@ -12627,7 +12859,7 @@ function createWebMcpHandler(platform2, options) {
12627
12859
  annotations: mapped
12628
12860
  });
12629
12861
  }
12630
- return rpcTools;
12862
+ return { tools: rpcTools, hidden };
12631
12863
  }
12632
12864
  async function callTool(name, args2) {
12633
12865
  const { tools, error } = await listRaw(true);
@@ -12652,11 +12884,18 @@ function createWebMcpHandler(platform2, options) {
12652
12884
  argv,
12653
12885
  stdinData,
12654
12886
  options.callTimeoutMs,
12655
- options.maxOutput
12887
+ options.maxOutput,
12888
+ watch
12656
12889
  );
12657
12890
  if (result.exitCode === -1) {
12658
12891
  throw new Error(result.stderr || "playwright-cli not found");
12659
12892
  }
12893
+ if (result.blocked) {
12894
+ throw new Error(dialogMessage(tab, result.blocked));
12895
+ }
12896
+ if (result.exitCode === 124) {
12897
+ throw new Error(timeoutMessage("webmcp-call", options.callTimeoutMs, tab, watcher));
12898
+ }
12660
12899
  if (result.exitCode === 2) {
12661
12900
  throw new Error(result.stderr || "webmcp-call error");
12662
12901
  }
@@ -12678,7 +12917,10 @@ function createWebMcpHandler(platform2, options) {
12678
12917
  function invalidateCache() {
12679
12918
  cache = void 0;
12680
12919
  }
12681
- return { getRpcTools, callTool, invalidateCache };
12920
+ function close() {
12921
+ watcher?.close();
12922
+ }
12923
+ return { getRpcTools, callTool, invalidateCache, close };
12682
12924
  }
12683
12925
 
12684
12926
  // src/serve/runtime.ts
@@ -13441,23 +13683,42 @@ async function buildProxyConnectOpts(platform2, options, version) {
13441
13683
  }
13442
13684
  return { connectOpts, hiddenValues };
13443
13685
  }
13444
- function resolvePlaywrightCli(platform2, explicit) {
13686
+ async function resolvePlaywrightCli(platform2, explicit) {
13445
13687
  if (explicit) return explicit;
13446
13688
  if (platform2.kind === "kernel") return "playwright-cli";
13447
13689
  const pathEnv = platform2.env.PATH ?? "";
13448
- const sep = pathEnv.includes(";") ? ";" : ":";
13449
- for (const dir of pathEnv.split(sep)) {
13690
+ const windows = pathEnv.includes(";");
13691
+ const names = windows ? ["playwright-cli.exe", "playwright-cli.cmd", "playwright-cli"] : ["playwright-cli"];
13692
+ for (const dir of pathEnv.split(windows ? ";" : ":")) {
13450
13693
  if (!dir) continue;
13451
- return "playwright-cli";
13694
+ for (const name of names) {
13695
+ const candidate = `${dir.replace(/[\\/]+$/, "")}/${name}`;
13696
+ if (await platform2.fs.exists(candidate).catch(() => false)) return "playwright-cli";
13697
+ }
13452
13698
  }
13453
13699
  return void 0;
13454
13700
  }
13701
+ async function listenOrReport(platform2, options, label) {
13702
+ try {
13703
+ return await platform2.listen({ host: options.host, port: options.port });
13704
+ } catch (err) {
13705
+ const { code: code2, message } = err;
13706
+ const reason = code2 === "EADDRINUSE" || /EADDRINUSE/.test(String(message)) ? "address already in use" : String(message ?? err);
13707
+ const host = options.host.includes(":") ? `[${options.host}]` : options.host;
13708
+ await platform2.write(
13709
+ 2,
13710
+ sanitizeTerminal(`${label}: cannot listen on ${host}:${options.port}: ${reason}
13711
+ `)
13712
+ );
13713
+ return 3;
13714
+ }
13715
+ }
13455
13716
  async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
13456
- const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13717
+ const cliPath = await resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13457
13718
  if (!cliPath) {
13458
13719
  await platform2.write(
13459
13720
  2,
13460
- "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13721
+ "mcpcat serve --webmcp: playwright-cli not found on PATH. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13461
13722
  );
13462
13723
  return 3;
13463
13724
  }
@@ -13492,6 +13753,20 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
13492
13753
  callTimeoutMs: options.callTimeout,
13493
13754
  maxOutput: options.maxOutput
13494
13755
  });
13756
+ try {
13757
+ return await serveWebMcpHandler(
13758
+ platform2,
13759
+ options,
13760
+ handler,
13761
+ version,
13762
+ resolvedBearer,
13763
+ generatedBearer
13764
+ );
13765
+ } finally {
13766
+ handler.close();
13767
+ }
13768
+ }
13769
+ async function serveWebMcpHandler(platform2, options, handler, version, resolvedBearer, generatedBearer) {
13495
13770
  const probe = await handler.getRpcTools();
13496
13771
  if (probe === null) {
13497
13772
  await platform2.write(
@@ -13501,7 +13776,7 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
13501
13776
  return 3;
13502
13777
  }
13503
13778
  const serverName = "mcpcat-webmcp";
13504
- const getTools = async () => await handler.getRpcTools() ?? [];
13779
+ const getTools = async () => await handler.getRpcTools() ?? { tools: [] };
13505
13780
  const callTool = (name, args2) => handler.callTool(name, args2);
13506
13781
  if (options.transport === "stdio") {
13507
13782
  return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
@@ -13532,8 +13807,7 @@ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version
13532
13807
  const line = buffer.slice(0, nlIdx).trim();
13533
13808
  buffer = buffer.slice(nlIdx + 1);
13534
13809
  if (!line) continue;
13535
- const tools = await getTools();
13536
- const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
13810
+ const outcome = await handleJsonRpc(line, getTools, callTool, serverName, version);
13537
13811
  if (outcome.status === 202) continue;
13538
13812
  const writer = platform2.stdout.getWriter();
13539
13813
  await writer.write(enc.encode(`${outcome.body}
@@ -13560,7 +13834,8 @@ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName,
13560
13834
  return 2;
13561
13835
  }
13562
13836
  const allowedOrigins = new Set(options.allowOrigins);
13563
- const listener = await platform2.listen({ host: options.host, port: options.port });
13837
+ const listener = await listenOrReport(platform2, options, "mcpcat serve --webmcp");
13838
+ if (typeof listener === "number") return listener;
13564
13839
  const actualPort = listener.port;
13565
13840
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13566
13841
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -13666,8 +13941,7 @@ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName,
13666
13941
  };
13667
13942
  }
13668
13943
  }
13669
- const tools = await getTools();
13670
- const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
13944
+ const outcome = await handleJsonRpc(request.body, getTools, callTool, serverName, version);
13671
13945
  return {
13672
13946
  status: outcome.status,
13673
13947
  reason: outcome.status === 200 ? "OK" : "Accepted",
@@ -13755,10 +14029,11 @@ async function runProxyHttp(platform2, options, proxy, resolvedBearer, generated
13755
14029
  return 2;
13756
14030
  }
13757
14031
  const allowedOrigins = new Set(options.allowOrigins);
13758
- const listener = await platform2.listen({
13759
- host: options.host,
13760
- port: options.port
13761
- });
14032
+ const listener = await listenOrReport(platform2, options, "mcpcat serve --proxy");
14033
+ if (typeof listener === "number") {
14034
+ await proxy.shutdown();
14035
+ return listener;
14036
+ }
13762
14037
  const actualPort = listener.port;
13763
14038
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13764
14039
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -13939,7 +14214,8 @@ async function runHttpServer(platform2, options, tools, call, serverName, versio
13939
14214
  return 2;
13940
14215
  }
13941
14216
  const allowedOrigins = new Set(options.allowOrigins);
13942
- const listener = await platform2.listen({ host: options.host, port: options.port });
14217
+ const listener = await listenOrReport(platform2, options, "mcpcat serve");
14218
+ if (typeof listener === "number") return listener;
13943
14219
  const actualPort = listener.port;
13944
14220
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13945
14221
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -16071,6 +16347,11 @@ async function runCall(platform2, conn, toolName, dataArg, toolArgs, opts) {
16071
16347
  if (toolArgs.length > 0) {
16072
16348
  const tools = await conn.listTools();
16073
16349
  const tool = tools.find((t) => t.name === toolName);
16350
+ const hidden = tool ? void 0 : conn.hiddenTool?.(toolName);
16351
+ if (hidden) {
16352
+ await writeErr(platform2, opts.json ?? false, "usage", hidden);
16353
+ return EXIT_USAGE;
16354
+ }
16074
16355
  const schema = tool?.inputSchema ?? { type: "object", properties: {} };
16075
16356
  const skipRequired = opts.noValidate || !!dataArg;
16076
16357
  const c = coerceArgsBySchema(toolArgs, schema, { noValidate: skipRequired });
@@ -17225,6 +17506,34 @@ function nodeListen(options) {
17225
17506
  });
17226
17507
  });
17227
17508
  }
17509
+ function hostWebSocket(url) {
17510
+ return new Promise((resolve, reject) => {
17511
+ const ws = new WebSocket(url);
17512
+ ws.binaryType = "arraybuffer";
17513
+ let feed;
17514
+ const readable = new ReadableStream({
17515
+ start(controller) {
17516
+ feed = controller;
17517
+ }
17518
+ });
17519
+ ws.onclose = () => feed.close();
17520
+ ws.onerror = () => reject(new Error(`websocket ${url} failed`));
17521
+ ws.onmessage = (event) => {
17522
+ feed.enqueue(typeof event.data === "string" ? event.data : new Uint8Array(event.data));
17523
+ };
17524
+ ws.onopen = () => {
17525
+ resolve({
17526
+ readable,
17527
+ writable: new WritableStream({
17528
+ write(chunk) {
17529
+ ws.send(chunk);
17530
+ }
17531
+ }),
17532
+ close: () => ws.close()
17533
+ });
17534
+ };
17535
+ });
17536
+ }
17228
17537
  function createHostPlatform() {
17229
17538
  const encoder2 = new TextEncoder();
17230
17539
  return {
@@ -17232,6 +17541,7 @@ function createHostPlatform() {
17232
17541
  fetch: globalThis.fetch,
17233
17542
  spawn: nodeSpawn,
17234
17543
  listen: nodeListen,
17544
+ websocket: hostWebSocket,
17235
17545
  fs: hostFs,
17236
17546
  env: process.env,
17237
17547
  cwd: process.cwd(),
@@ -17269,5 +17579,5 @@ try {
17269
17579
  });
17270
17580
  } catch {
17271
17581
  }
17272
- var code = await run(platform, "0.20.2", args);
17582
+ var code = await run(platform, "0.20.3", args);
17273
17583
  process.exit(code);
@@ -8106,8 +8106,19 @@ function buildServerInfo(discoverResult2) {
8106
8106
  instructions: discoverResult2?.instructions
8107
8107
  };
8108
8108
  }
8109
+ function readHiddenTools(result) {
8110
+ const meta = result?._meta;
8111
+ const hidden = meta?.["io.mcpcat/hidden"];
8112
+ const out = {};
8113
+ if (!hidden || typeof hidden !== "object" || Array.isArray(hidden)) return out;
8114
+ for (const [name, why] of Object.entries(hidden)) {
8115
+ if (typeof why === "string") out[name] = why;
8116
+ }
8117
+ return out;
8118
+ }
8109
8119
  function buildStatelessMethods(requestFn, discoverResult2) {
8110
8120
  let serverInfoCache;
8121
+ let hidden = {};
8111
8122
  return {
8112
8123
  async info() {
8113
8124
  if (serverInfoCache) return serverInfoCache;
@@ -8115,7 +8126,12 @@ function buildStatelessMethods(requestFn, discoverResult2) {
8115
8126
  return serverInfoCache;
8116
8127
  },
8117
8128
  async listTools() {
8118
- return (await requestFn("tools/list", {}))?.tools ?? [];
8129
+ const result = await requestFn("tools/list", {});
8130
+ hidden = readHiddenTools(result);
8131
+ return result?.tools ?? [];
8132
+ },
8133
+ hiddenTool(name) {
8134
+ return Object.hasOwn(hidden, name) ? hidden[name] : void 0;
8119
8135
  },
8120
8136
  async callTool(name, args, _callOptions) {
8121
8137
  const params = {
@@ -8555,12 +8571,18 @@ function buildServerInfo2(discoverResult2) {
8555
8571
  };
8556
8572
  }
8557
8573
  function buildConnectionMethods(rpc, discoverResult2, proc) {
8574
+ let hidden = {};
8558
8575
  return {
8559
8576
  async info() {
8560
8577
  return buildServerInfo2(discoverResult2);
8561
8578
  },
8562
8579
  async listTools() {
8563
- return (await rpc.request("tools/list", {}))?.tools ?? [];
8580
+ const result = await rpc.request("tools/list", {});
8581
+ hidden = readHiddenTools(result);
8582
+ return result?.tools ?? [];
8583
+ },
8584
+ hiddenTool(name) {
8585
+ return Object.hasOwn(hidden, name) ? hidden[name] : void 0;
8564
8586
  },
8565
8587
  async callTool(name, args, callOptions) {
8566
8588
  const params = { name, arguments: args ?? {} };
@@ -8762,7 +8784,7 @@ function readLine(platform2) {
8762
8784
  return new Promise((resolve2, reject) => {
8763
8785
  const reader = platform2.stdin.getReader();
8764
8786
  const chunks = [];
8765
- function pump() {
8787
+ function pump2() {
8766
8788
  reader.read().then(({ done, value }) => {
8767
8789
  if (done) {
8768
8790
  reader.releaseLock();
@@ -8776,10 +8798,10 @@ function readLine(platform2) {
8776
8798
  resolve2(text.split("\n")[0]);
8777
8799
  return;
8778
8800
  }
8779
- pump();
8801
+ pump2();
8780
8802
  }, reject);
8781
8803
  }
8782
- pump();
8804
+ pump2();
8783
8805
  });
8784
8806
  }
8785
8807
  function concatChunks(chunks) {
@@ -11658,7 +11680,7 @@ async function writeResponse(writer, response) {
11658
11680
  var PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-06-18"];
11659
11681
  var OUTPUT_CAP = 1048576;
11660
11682
  var JSON_CT = "application/json; charset=utf-8";
11661
- async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
11683
+ async function handleJsonRpc(raw, source, call, serverName, serverVersion) {
11662
11684
  let parsed;
11663
11685
  try {
11664
11686
  parsed = JSON.parse(raw);
@@ -11675,16 +11697,26 @@ async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
11675
11697
  if (method === "initialize")
11676
11698
  return rpcOk(id, initializeResult(params, serverName, serverVersion));
11677
11699
  if (method === "server/discover") return rpcOk(id, discoverResult(serverName, serverVersion));
11678
- if (method === "tools/list") return rpcOk(id, { tools });
11679
11700
  if (method === "ping") return rpcOk(id, {});
11680
- if (method === "tools/call") return toolCall(id, params, tools, call);
11701
+ if (method === "tools/list" || method === "tools/call") {
11702
+ const listing = Array.isArray(source) ? { tools: source } : await source();
11703
+ if (listing.error) return rpcError(id, -32e3, listing.error);
11704
+ if (method === "tools/call") return toolCall(id, params, listing, call);
11705
+ const hidden = listing.hidden && Object.keys(listing.hidden).length > 0 ? listing.hidden : null;
11706
+ return rpcOk(id, {
11707
+ tools: listing.tools,
11708
+ ...hidden ? { _meta: { "io.mcpcat/hidden": hidden } } : {}
11709
+ });
11710
+ }
11681
11711
  return rpcError(id, -32601, `Method not found: ${method}`);
11682
11712
  }
11683
- async function toolCall(id, params, tools, call) {
11713
+ async function toolCall(id, params, { tools, hidden }, call) {
11684
11714
  const name = params && typeof params === "object" ? readString(params, "name") : void 0;
11685
11715
  if (!name) return rpcError(id, -32602, "tools/call requires a name");
11686
- if (!tools.some((tool) => tool.name === name))
11687
- return rpcError(id, -32602, `Unknown tool: ${name}`);
11716
+ if (!tools.some((tool) => tool.name === name)) {
11717
+ const why = hidden && Object.hasOwn(hidden, name) ? hidden[name] : void 0;
11718
+ return rpcError(id, -32602, why ? `Unknown tool: ${name}: ${why}` : `Unknown tool: ${name}`);
11719
+ }
11688
11720
  const args = params && typeof params === "object" ? Object.getOwnPropertyDescriptor(params, "arguments")?.value : void 0;
11689
11721
  try {
11690
11722
  const result = await call(name, args);
@@ -12048,7 +12080,149 @@ ${tail}`,
12048
12080
  }
12049
12081
  };
12050
12082
  }
12051
- async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12083
+ var DIALOG_POLL_MS = 250;
12084
+ var DIALOG_READY_MS = 1e3;
12085
+ var DIALOG_STALL_MS = 1e3;
12086
+ function watchLoop(watch) {
12087
+ let timer;
12088
+ const result = new Promise((resolve2) => {
12089
+ timer = setInterval(() => {
12090
+ const found = watch();
12091
+ if (found) {
12092
+ clearInterval(timer);
12093
+ resolve2(found);
12094
+ }
12095
+ }, DIALOG_POLL_MS);
12096
+ });
12097
+ return {
12098
+ result,
12099
+ stop() {
12100
+ clearInterval(timer);
12101
+ }
12102
+ };
12103
+ }
12104
+ async function browserSocketUrl(platform2, cdp) {
12105
+ if (/^wss?:\/\//i.test(cdp)) return cdp;
12106
+ if (!/^https?:\/\//i.test(cdp)) return void 0;
12107
+ const res = await platform2.fetch(`${cdp.replace(/\/+$/, "")}/json/version`, {
12108
+ signal: AbortSignal.timeout(DIALOG_READY_MS)
12109
+ });
12110
+ const body = await res.json();
12111
+ return typeof body.webSocketDebuggerUrl === "string" ? body.webSocketDebuggerUrl : void 0;
12112
+ }
12113
+ function parseCdp(data) {
12114
+ try {
12115
+ return JSON.parse(
12116
+ typeof data === "string" ? data : new TextDecoder().decode(data)
12117
+ );
12118
+ } catch {
12119
+ return void 0;
12120
+ }
12121
+ }
12122
+ function applyCdp(state, channel, send, msg, settle) {
12123
+ if (msg.method === "Page.javascriptDialogOpening") {
12124
+ state.open = typeof msg.params?.type === "string" ? msg.params.type : "dialog";
12125
+ } else if (msg.method === "Page.javascriptDialogClosed") {
12126
+ state.open = void 0;
12127
+ } else if (msg.method === "Target.detachedFromTarget") {
12128
+ channel.close();
12129
+ } else if (msg.id === 1) {
12130
+ const sessionId = msg.result?.sessionId;
12131
+ if (typeof sessionId !== "string") {
12132
+ channel.close();
12133
+ return;
12134
+ }
12135
+ state.enableSentAt = Date.now();
12136
+ send({ id: 2, sessionId, method: "Page.enable" });
12137
+ } else if (msg.id === 2) {
12138
+ state.enableSentAt = void 0;
12139
+ settle();
12140
+ }
12141
+ }
12142
+ async function pump(state, channel, send, settle) {
12143
+ const reader = channel.readable.getReader();
12144
+ try {
12145
+ for (; ; ) {
12146
+ const { done, value } = await reader.read();
12147
+ if (done) return;
12148
+ const msg = parseCdp(value);
12149
+ if (msg) applyCdp(state, channel, send, msg, settle);
12150
+ }
12151
+ } catch {
12152
+ return;
12153
+ }
12154
+ }
12155
+ async function attachSocket(platform2, state, url, targetId) {
12156
+ const channel = await platform2.websocket(url).catch(() => void 0);
12157
+ if (!channel) return;
12158
+ state.channel = channel;
12159
+ const writer = channel.writable.getWriter();
12160
+ const send = (message) => {
12161
+ writer.write(JSON.stringify(message)).catch(() => channel.close());
12162
+ };
12163
+ let settle;
12164
+ const settled = new Promise((resolve2) => {
12165
+ settle = resolve2;
12166
+ });
12167
+ void pump(state, channel, send, settle).then(() => {
12168
+ if (state.channel === channel) {
12169
+ state.channel = void 0;
12170
+ state.open = void 0;
12171
+ state.enableSentAt = void 0;
12172
+ }
12173
+ channel.close();
12174
+ settle();
12175
+ });
12176
+ send({ id: 1, method: "Target.attachToTarget", params: { targetId, flatten: true } });
12177
+ await settled;
12178
+ }
12179
+ function watchJsDialogs(platform2, cdp, targetId) {
12180
+ const state = {};
12181
+ let connecting;
12182
+ let closed = false;
12183
+ const connect2 = async () => {
12184
+ if (!platform2.websocket) return;
12185
+ const url = await browserSocketUrl(platform2, cdp).catch(() => void 0);
12186
+ if (!url || closed) return;
12187
+ await attachSocket(platform2, state, url, targetId);
12188
+ };
12189
+ return {
12190
+ async ready() {
12191
+ if (closed || state.channel) return;
12192
+ connecting ??= connect2().finally(() => {
12193
+ connecting = void 0;
12194
+ });
12195
+ let timer;
12196
+ await Promise.race([
12197
+ connecting,
12198
+ new Promise((resolve2) => {
12199
+ timer = setTimeout(resolve2, DIALOG_READY_MS);
12200
+ })
12201
+ ]);
12202
+ clearTimeout(timer);
12203
+ },
12204
+ dialog: () => state.open,
12205
+ stalled: () => state.enableSentAt !== void 0 && Date.now() - state.enableSentAt >= DIALOG_STALL_MS,
12206
+ close() {
12207
+ closed = true;
12208
+ state.channel?.close();
12209
+ }
12210
+ };
12211
+ }
12212
+ function dialogMessage(tab, type) {
12213
+ return `tab ${tab} is blocked by a JavaScript ${type.replace(/[^a-z]/gi, "")} dialog; answer or dismiss it in the browser, then retry`;
12214
+ }
12215
+ function timeoutMessage(verb, timeoutMs, tab, watcher) {
12216
+ const head = `${verb} timed out after ${Math.round(timeoutMs / 1e3)}s`;
12217
+ if (!watcher) {
12218
+ return `${head}; if the page shows a JavaScript dialog (alert, confirm, prompt), answer it, and pass --cdp <url> so mcpcat can detect one`;
12219
+ }
12220
+ if (watcher.stalled()) {
12221
+ return `${head}; tab ${tab} does not answer the DevTools protocol, so a JavaScript dialog opened before mcpcat attached may block it; answer it in the browser, then retry`;
12222
+ }
12223
+ return head;
12224
+ }
12225
+ async function runProcess(platform2, argv, stdin, timeoutMs, limit, watch) {
12052
12226
  let proc;
12053
12227
  try {
12054
12228
  proc = await platform2.spawn({ argv });
@@ -12092,12 +12266,20 @@ async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12092
12266
  readStream3(proc.stderr, err),
12093
12267
  proc.wait()
12094
12268
  ]);
12095
- const result = await Promise.race([work, timer]);
12269
+ const watcher = watch ? watchLoop(watch) : void 0;
12270
+ const blocked = watcher ? watcher.result.then((type) => ({ blocked: type })) : new Promise(() => {
12271
+ });
12272
+ const result = await Promise.race([work, timer, blocked]);
12096
12273
  clearTimeout(timeoutId);
12274
+ watcher?.stop();
12097
12275
  if (result === "timeout") {
12098
12276
  await proc.kill("SIGTERM");
12099
12277
  return { stdout: "", stderr: "timed out", exitCode: 124, cut: false };
12100
12278
  }
12279
+ if (!Array.isArray(result)) {
12280
+ await proc.kill("SIGTERM");
12281
+ return { stdout: "", stderr: "blocked", exitCode: 124, cut: false, blocked: result.blocked };
12282
+ }
12101
12283
  const [, , exitResult] = result;
12102
12284
  const stdout = out.finish();
12103
12285
  const stderr = err.finish();
@@ -12108,7 +12290,7 @@ async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
12108
12290
  cut: stdout.cut
12109
12291
  };
12110
12292
  }
12111
- async function resolveWebMcpTab(platform2, options) {
12293
+ async function listTabs(platform2, options) {
12112
12294
  const argv = [options.playwrightCli, "tab-list"];
12113
12295
  if (options.cdp) argv.push("--cdp", options.cdp);
12114
12296
  const result = await runProcess(
@@ -12135,6 +12317,13 @@ async function resolveWebMcpTab(platform2, options) {
12135
12317
  if (new Set(rows.map((r) => r.id)).size !== rows.length || active.length > 1) {
12136
12318
  return { error: "could not read playwright-cli tab-list" };
12137
12319
  }
12320
+ return { rows };
12321
+ }
12322
+ async function resolveWebMcpTab(platform2, options) {
12323
+ const listed = await listTabs(platform2, options);
12324
+ if ("error" in listed) return listed;
12325
+ const { rows } = listed;
12326
+ const active = rows.filter((r) => r.active);
12138
12327
  if (active.length === 1) return { tab: active[0].id };
12139
12328
  if (rows.length === 1) return { tab: rows[0].id };
12140
12329
  return {
@@ -12197,20 +12386,57 @@ function ambiguityError(name, peers) {
12197
12386
  `tool "${name}" is ambiguous: ${peers.length} tools on the page match it (${origins}), and playwright-cli runs the first tool by name and cannot select one by origin, so mcpcat refuses the call`
12198
12387
  );
12199
12388
  }
12389
+ async function blockedNow(watcher, tab) {
12390
+ if (!watcher) return void 0;
12391
+ await watcher.ready();
12392
+ const open = watcher.dialog();
12393
+ return open ? dialogMessage(tab, open) : void 0;
12394
+ }
12395
+ async function tabGone(platform2, options) {
12396
+ const { tab } = options;
12397
+ if (!tab) return void 0;
12398
+ const listed = await listTabs(platform2, options);
12399
+ if ("error" in listed || listed.rows.some((r) => r.id === tab)) return void 0;
12400
+ return `tab ${tab} is gone (the browser restarted or the tab closed); restart mcpcat serve --webmcp, or pass --tab <targetId> (see playwright-cli tab-list)`;
12401
+ }
12402
+ function hiddenToolMessage(name) {
12403
+ return `tool ${name} exists on the page but is hidden because it is consequential (consequentialHint); restart with mcpcat serve --webmcp --allow-consequential to expose it`;
12404
+ }
12200
12405
  function createWebMcpHandler(platform2, options) {
12201
12406
  let cache;
12202
12407
  const baseArgs = buildBaseArgs(options);
12203
12408
  const listLimit = Math.max(options.maxOutput, WEBMCP_LIST_FLOOR);
12409
+ const { cdp, tab } = options;
12410
+ const watcher = cdp && tab ? watchJsDialogs(platform2, cdp, tab) : void 0;
12411
+ const watch = watcher ? () => watcher.dialog() : void 0;
12204
12412
  async function listRaw(fresh = false) {
12413
+ const blocked = await blockedNow(watcher, tab);
12414
+ if (blocked) return { tools: [], error: blocked };
12205
12415
  const now = Date.now();
12206
12416
  if (!fresh && cache && now < cache.expiresAt) {
12207
12417
  return { tools: cache.tools };
12208
12418
  }
12209
12419
  const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
12210
- const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs, listLimit);
12420
+ const result = await runProcess(
12421
+ platform2,
12422
+ argv,
12423
+ void 0,
12424
+ options.callTimeoutMs,
12425
+ listLimit,
12426
+ watch
12427
+ );
12211
12428
  if (result.exitCode === -1) {
12212
12429
  return { tools: [], error: result.stderr, notFound: true };
12213
12430
  }
12431
+ if (result.blocked) {
12432
+ return { tools: [], error: dialogMessage(tab, result.blocked) };
12433
+ }
12434
+ if (result.exitCode === 124) {
12435
+ return {
12436
+ tools: [],
12437
+ error: timeoutMessage("webmcp-list", options.callTimeoutMs, tab, watcher)
12438
+ };
12439
+ }
12214
12440
  if (result.exitCode !== 0) {
12215
12441
  return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
12216
12442
  }
@@ -12226,6 +12452,10 @@ function createWebMcpHandler(platform2, options) {
12226
12452
  if (!Array.isArray(parsed)) {
12227
12453
  return { tools: [], error: "webmcp-list did not return an array" };
12228
12454
  }
12455
+ if (parsed.length === 0) {
12456
+ const gone2 = await tabGone(platform2, options);
12457
+ if (gone2) return { tools: [], error: gone2 };
12458
+ }
12229
12459
  const tools = parsed;
12230
12460
  cache = { tools, expiresAt: now + options.ttlMs };
12231
12461
  return { tools };
@@ -12233,11 +12463,13 @@ function createWebMcpHandler(platform2, options) {
12233
12463
  async function getRpcTools() {
12234
12464
  const { tools, error, notFound } = await listRaw();
12235
12465
  if (notFound) return null;
12236
- if (error) return [];
12466
+ if (error) return { tools: [], error };
12237
12467
  const rpcTools = [];
12468
+ const hidden = {};
12238
12469
  for (const { name, tool, peers } of exposedTools(tools)) {
12239
12470
  if (peers.length > 1) continue;
12240
12471
  if (tool.annotations?.consequentialHint && !options.allowConsequential) {
12472
+ hidden[name] = hiddenToolMessage(name);
12241
12473
  continue;
12242
12474
  }
12243
12475
  const mapped = mapDescriptionAnnotations(tool.annotations);
@@ -12248,7 +12480,7 @@ function createWebMcpHandler(platform2, options) {
12248
12480
  annotations: mapped
12249
12481
  });
12250
12482
  }
12251
- return rpcTools;
12483
+ return { tools: rpcTools, hidden };
12252
12484
  }
12253
12485
  async function callTool(name, args) {
12254
12486
  const { tools, error } = await listRaw(true);
@@ -12273,11 +12505,18 @@ function createWebMcpHandler(platform2, options) {
12273
12505
  argv,
12274
12506
  stdinData,
12275
12507
  options.callTimeoutMs,
12276
- options.maxOutput
12508
+ options.maxOutput,
12509
+ watch
12277
12510
  );
12278
12511
  if (result.exitCode === -1) {
12279
12512
  throw new Error(result.stderr || "playwright-cli not found");
12280
12513
  }
12514
+ if (result.blocked) {
12515
+ throw new Error(dialogMessage(tab, result.blocked));
12516
+ }
12517
+ if (result.exitCode === 124) {
12518
+ throw new Error(timeoutMessage("webmcp-call", options.callTimeoutMs, tab, watcher));
12519
+ }
12281
12520
  if (result.exitCode === 2) {
12282
12521
  throw new Error(result.stderr || "webmcp-call error");
12283
12522
  }
@@ -12299,7 +12538,10 @@ function createWebMcpHandler(platform2, options) {
12299
12538
  function invalidateCache() {
12300
12539
  cache = void 0;
12301
12540
  }
12302
- return { getRpcTools, callTool, invalidateCache };
12541
+ function close() {
12542
+ watcher?.close();
12543
+ }
12544
+ return { getRpcTools, callTool, invalidateCache, close };
12303
12545
  }
12304
12546
 
12305
12547
  // src/serve/runtime.ts
@@ -13062,23 +13304,42 @@ async function buildProxyConnectOpts(platform2, options, version) {
13062
13304
  }
13063
13305
  return { connectOpts, hiddenValues };
13064
13306
  }
13065
- function resolvePlaywrightCli(platform2, explicit) {
13307
+ async function resolvePlaywrightCli(platform2, explicit) {
13066
13308
  if (explicit) return explicit;
13067
13309
  if (platform2.kind === "kernel") return "playwright-cli";
13068
13310
  const pathEnv = platform2.env.PATH ?? "";
13069
- const sep = pathEnv.includes(";") ? ";" : ":";
13070
- for (const dir of pathEnv.split(sep)) {
13311
+ const windows = pathEnv.includes(";");
13312
+ const names = windows ? ["playwright-cli.exe", "playwright-cli.cmd", "playwright-cli"] : ["playwright-cli"];
13313
+ for (const dir of pathEnv.split(windows ? ";" : ":")) {
13071
13314
  if (!dir) continue;
13072
- return "playwright-cli";
13315
+ for (const name of names) {
13316
+ const candidate = `${dir.replace(/[\\/]+$/, "")}/${name}`;
13317
+ if (await platform2.fs.exists(candidate).catch(() => false)) return "playwright-cli";
13318
+ }
13073
13319
  }
13074
13320
  return void 0;
13075
13321
  }
13322
+ async function listenOrReport(platform2, options, label) {
13323
+ try {
13324
+ return await platform2.listen({ host: options.host, port: options.port });
13325
+ } catch (err) {
13326
+ const { code, message } = err;
13327
+ const reason = code === "EADDRINUSE" || /EADDRINUSE/.test(String(message)) ? "address already in use" : String(message ?? err);
13328
+ const host = options.host.includes(":") ? `[${options.host}]` : options.host;
13329
+ await platform2.write(
13330
+ 2,
13331
+ sanitizeTerminal(`${label}: cannot listen on ${host}:${options.port}: ${reason}
13332
+ `)
13333
+ );
13334
+ return 3;
13335
+ }
13336
+ }
13076
13337
  async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
13077
- const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13338
+ const cliPath = await resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13078
13339
  if (!cliPath) {
13079
13340
  await platform2.write(
13080
13341
  2,
13081
- "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13342
+ "mcpcat serve --webmcp: playwright-cli not found on PATH. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13082
13343
  );
13083
13344
  return 3;
13084
13345
  }
@@ -13113,6 +13374,20 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
13113
13374
  callTimeoutMs: options.callTimeout,
13114
13375
  maxOutput: options.maxOutput
13115
13376
  });
13377
+ try {
13378
+ return await serveWebMcpHandler(
13379
+ platform2,
13380
+ options,
13381
+ handler,
13382
+ version,
13383
+ resolvedBearer,
13384
+ generatedBearer
13385
+ );
13386
+ } finally {
13387
+ handler.close();
13388
+ }
13389
+ }
13390
+ async function serveWebMcpHandler(platform2, options, handler, version, resolvedBearer, generatedBearer) {
13116
13391
  const probe = await handler.getRpcTools();
13117
13392
  if (probe === null) {
13118
13393
  await platform2.write(
@@ -13122,7 +13397,7 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
13122
13397
  return 3;
13123
13398
  }
13124
13399
  const serverName = "mcpcat-webmcp";
13125
- const getTools = async () => await handler.getRpcTools() ?? [];
13400
+ const getTools = async () => await handler.getRpcTools() ?? { tools: [] };
13126
13401
  const callTool = (name, args) => handler.callTool(name, args);
13127
13402
  if (options.transport === "stdio") {
13128
13403
  return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
@@ -13153,8 +13428,7 @@ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version
13153
13428
  const line = buffer.slice(0, nlIdx).trim();
13154
13429
  buffer = buffer.slice(nlIdx + 1);
13155
13430
  if (!line) continue;
13156
- const tools = await getTools();
13157
- const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
13431
+ const outcome = await handleJsonRpc(line, getTools, callTool, serverName, version);
13158
13432
  if (outcome.status === 202) continue;
13159
13433
  const writer = platform2.stdout.getWriter();
13160
13434
  await writer.write(enc.encode(`${outcome.body}
@@ -13181,7 +13455,8 @@ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName,
13181
13455
  return 2;
13182
13456
  }
13183
13457
  const allowedOrigins = new Set(options.allowOrigins);
13184
- const listener = await platform2.listen({ host: options.host, port: options.port });
13458
+ const listener = await listenOrReport(platform2, options, "mcpcat serve --webmcp");
13459
+ if (typeof listener === "number") return listener;
13185
13460
  const actualPort = listener.port;
13186
13461
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13187
13462
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -13287,8 +13562,7 @@ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName,
13287
13562
  };
13288
13563
  }
13289
13564
  }
13290
- const tools = await getTools();
13291
- const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
13565
+ const outcome = await handleJsonRpc(request.body, getTools, callTool, serverName, version);
13292
13566
  return {
13293
13567
  status: outcome.status,
13294
13568
  reason: outcome.status === 200 ? "OK" : "Accepted",
@@ -13376,10 +13650,11 @@ async function runProxyHttp(platform2, options, proxy, resolvedBearer, generated
13376
13650
  return 2;
13377
13651
  }
13378
13652
  const allowedOrigins = new Set(options.allowOrigins);
13379
- const listener = await platform2.listen({
13380
- host: options.host,
13381
- port: options.port
13382
- });
13653
+ const listener = await listenOrReport(platform2, options, "mcpcat serve --proxy");
13654
+ if (typeof listener === "number") {
13655
+ await proxy.shutdown();
13656
+ return listener;
13657
+ }
13383
13658
  const actualPort = listener.port;
13384
13659
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13385
13660
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -13560,7 +13835,8 @@ async function runHttpServer(platform2, options, tools, call, serverName, versio
13560
13835
  return 2;
13561
13836
  }
13562
13837
  const allowedOrigins = new Set(options.allowOrigins);
13563
- const listener = await platform2.listen({ host: options.host, port: options.port });
13838
+ const listener = await listenOrReport(platform2, options, "mcpcat serve");
13839
+ if (typeof listener === "number") return listener;
13564
13840
  const actualPort = listener.port;
13565
13841
  const serverOrigins = buildServerOrigins(options.host, actualPort);
13566
13842
  const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
@@ -15692,6 +15968,11 @@ async function runCall(platform2, conn, toolName, dataArg, toolArgs, opts) {
15692
15968
  if (toolArgs.length > 0) {
15693
15969
  const tools = await conn.listTools();
15694
15970
  const tool = tools.find((t) => t.name === toolName);
15971
+ const hidden = tool ? void 0 : conn.hiddenTool?.(toolName);
15972
+ if (hidden) {
15973
+ await writeErr(platform2, opts.json ?? false, "usage", hidden);
15974
+ return EXIT_USAGE;
15975
+ }
15695
15976
  const schema = tool?.inputSchema ?? { type: "object", properties: {} };
15696
15977
  const skipRequired = opts.noValidate || !!dataArg;
15697
15978
  const c = coerceArgsBySchema(toolArgs, schema, { noValidate: skipRequired });
@@ -16956,6 +17237,7 @@ function createKernelPlatform(ctx, options) {
16956
17237
  fetch: createKernelFetch(ctx),
16957
17238
  spawn: (options2) => kernelSpawn(ctx, options2),
16958
17239
  listen: (options2) => kernelListen(ctx, options2),
17240
+ ...ctx.websocket ? { websocket: (url) => ctx.websocket(url) } : {},
16959
17241
  fs: kernelFs(ctx),
16960
17242
  env: ctx.env,
16961
17243
  cwd: ctx.cwd,
@@ -16991,7 +17273,7 @@ async function main(ctx) {
16991
17273
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
16992
17274
  });
16993
17275
  const args = ctx.argv.slice(1);
16994
- return run(platform2, "0.20.2", args);
17276
+ return run(platform2, "0.20.3", args);
16995
17277
  }
16996
17278
  export {
16997
17279
  main as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/mcpcat",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "A curl for the Model Context Protocol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",