smooth-operator-mcp 3.0.1 → 3.0.4

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.
@@ -397,7 +397,7 @@ var SERVER_VERSION;
397
397
  var init_version = __esm({
398
398
  "src/server/version.ts"() {
399
399
  "use strict";
400
- SERVER_VERSION = "3.0.1";
400
+ SERVER_VERSION = "3.0.4";
401
401
  }
402
402
  });
403
403
 
@@ -639,9 +639,9 @@ function resolveConfigPath(target, homeDirectory = homedir3(), environment = pro
639
639
  return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
640
640
  }
641
641
  if (platform2() === "win32") {
642
- return join6(environment.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
642
+ return join6(resolveConfigDirectory(environment.APPDATA, join6(home, "AppData", "Roaming")), "Claude", "claude_desktop_config.json");
643
643
  }
644
- return join6(environment.XDG_CONFIG_HOME ?? join6(home, ".config"), "Claude", "claude_desktop_config.json");
644
+ return join6(resolveConfigDirectory(environment.XDG_CONFIG_HOME, join6(home, ".config")), "Claude", "claude_desktop_config.json");
645
645
  }
646
646
  function resolveOpenCodeConfigPath(homeDirectory = homedir3(), environment = process.env, override) {
647
647
  if (override) {
@@ -650,9 +650,17 @@ function resolveOpenCodeConfigPath(homeDirectory = homedir3(), environment = pro
650
650
  if (environment.OPENCODE_CONFIG) {
651
651
  return resolve5(environment.OPENCODE_CONFIG);
652
652
  }
653
- const configDirectory = environment.OPENCODE_CONFIG_DIR ?? join6(homeDirectory || homedir3(), ".config", "opencode");
653
+ const configDirectory = resolveConfigDirectory(environment.OPENCODE_CONFIG_DIR, join6(homeDirectory || homedir3(), ".config", "opencode"));
654
654
  return join6(configDirectory, "opencode.json");
655
655
  }
656
+ function resolveConfigDirectory(value, fallback) {
657
+ const candidate = value?.trim() || fallback;
658
+ try {
659
+ return resolve5(candidate);
660
+ } catch (error) {
661
+ throw new AppError("INSTALL_CONFIG_INVALID", "Configuration directory paths must be valid filesystem paths.", { cause: error });
662
+ }
663
+ }
656
664
  async function installJsonConfig(target, plannedPath, options, allowOpenCodeJsoncFallback = false) {
657
665
  const path = target === "opencode" && allowOpenCodeJsoncFallback ? await chooseExistingOpenCodePath(plannedPath) : plannedPath;
658
666
  await ensureSecureDirectory(dirname4(path));
@@ -898,6 +906,9 @@ function sameOpenCodeEntry(value, desired) {
898
906
  }
899
907
  async function ensureSecureDirectory(path) {
900
908
  const absolute = resolve5(path);
909
+ if (parse3(absolute).root === absolute) {
910
+ throw new AppError("INSTALL_CONFIG_FAILED", "Configuration directories must not be filesystem roots.");
911
+ }
901
912
  await assertNoSymlinkComponents2(absolute, "configuration directory");
902
913
  await mkdir3(absolute, { recursive: true, mode: 448 });
903
914
  await assertNoSymlinkComponents2(absolute, "configuration directory");
@@ -1227,7 +1238,7 @@ __export(installer_wizard_exports, {
1227
1238
  runWizard: () => runWizard
1228
1239
  });
1229
1240
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7, parse as parse4, resolve as resolve6, win32 as win323 } from "node:path";
1230
- import { existsSync as existsSync2 } from "node:fs";
1241
+ import { accessSync as accessSync2, constants as constants3, statSync } from "node:fs";
1231
1242
  import { chmod as chmod3, lstat as lstat4, rename as rename4, unlink as unlink4, writeFile as writeFile2 } from "node:fs/promises";
1232
1243
  import { homedir as homedir4 } from "node:os";
1233
1244
  import { isIP as isIP3 } from "node:net";
@@ -1244,6 +1255,19 @@ function isAbsolutePath(value) {
1244
1255
  function isFilesystemRoot(value) {
1245
1256
  return isAbsolute4(value) && parse4(value).root === value || win323.isAbsolute(value) && win323.parse(value).root === value;
1246
1257
  }
1258
+ function isExecutableFile(path) {
1259
+ try {
1260
+ if (!statSync(path).isFile()) {
1261
+ return false;
1262
+ }
1263
+ if (process.platform !== "win32") {
1264
+ accessSync2(path, constants3.X_OK);
1265
+ }
1266
+ return true;
1267
+ } catch {
1268
+ return false;
1269
+ }
1270
+ }
1247
1271
  function isInteractive() {
1248
1272
  return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
1249
1273
  }
@@ -1313,8 +1337,8 @@ async function askBrowser(session, ui) {
1313
1337
  return detected[numeric - 1].path;
1314
1338
  }
1315
1339
  if (/^\d+$/.test(answer)) continue;
1316
- if (isAbsolutePath(answer) && existsSync2(answer)) return answer;
1317
- ui.failure("Enter a listed number or an existing absolute path.");
1340
+ if (isAbsolutePath(answer) && isExecutableFile(answer)) return answer;
1341
+ ui.failure("Enter a listed number or an existing executable file path.");
1318
1342
  }
1319
1343
  }
1320
1344
  while (true) {
@@ -1324,8 +1348,8 @@ async function askBrowser(session, ui) {
1324
1348
  ui.failure("Enter an absolute path.");
1325
1349
  continue;
1326
1350
  }
1327
- if (!existsSync2(answer)) {
1328
- ui.failure("That path does not exist.");
1351
+ if (!isExecutableFile(answer)) {
1352
+ ui.failure("That path is not an executable file.");
1329
1353
  continue;
1330
1354
  }
1331
1355
  return answer;
@@ -1560,16 +1584,65 @@ function writeSummary(ui, harness, choices) {
1560
1584
  ]);
1561
1585
  }
1562
1586
  async function defaultProbe(url, timeoutMs) {
1587
+ const controller = new AbortController();
1588
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1563
1589
  try {
1564
- const controller = new AbortController();
1565
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1566
1590
  const response = await fetch(url, { signal: controller.signal });
1567
- clearTimeout(timer);
1568
1591
  if (!response.ok) return { state: "no-file" };
1569
- const version = await response.json().catch(() => ({}));
1570
- return { state: "live", version };
1592
+ const version = await readProbeJson(response, controller.signal);
1593
+ return isDevToolsVersion(version) ? { state: "live", version } : { state: "no-file" };
1571
1594
  } catch {
1572
1595
  return { state: "no-file" };
1596
+ } finally {
1597
+ clearTimeout(timer);
1598
+ }
1599
+ }
1600
+ function isDevToolsVersion(value) {
1601
+ return isRecord4(value) && typeof value.Browser === "string" && typeof value.webSocketDebuggerUrl === "string" && /^wss?:\/\//i.test(value.webSocketDebuggerUrl);
1602
+ }
1603
+ async function readProbeJson(response, signal) {
1604
+ const declaredLength = Number(response.headers.get("content-length"));
1605
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_PROBE_RESPONSE_BYTES) {
1606
+ return void 0;
1607
+ }
1608
+ if (!response.body) {
1609
+ return void 0;
1610
+ }
1611
+ const reader = response.body.getReader();
1612
+ const chunks = [];
1613
+ let total = 0;
1614
+ try {
1615
+ while (true) {
1616
+ if (signal.aborted) {
1617
+ return void 0;
1618
+ }
1619
+ const next = await reader.read();
1620
+ if (next.done) {
1621
+ break;
1622
+ }
1623
+ if (!(next.value instanceof Uint8Array)) {
1624
+ return void 0;
1625
+ }
1626
+ total += next.value.byteLength;
1627
+ if (total > MAX_PROBE_RESPONSE_BYTES) {
1628
+ return void 0;
1629
+ }
1630
+ chunks.push(next.value);
1631
+ }
1632
+ } finally {
1633
+ await reader.cancel().catch(() => void 0);
1634
+ reader.releaseLock();
1635
+ }
1636
+ const bytes = new Uint8Array(total);
1637
+ let offset = 0;
1638
+ for (const chunk of chunks) {
1639
+ bytes.set(chunk, offset);
1640
+ offset += chunk.byteLength;
1641
+ }
1642
+ try {
1643
+ return JSON.parse(new TextDecoder().decode(bytes));
1644
+ } catch {
1645
+ return void 0;
1573
1646
  }
1574
1647
  }
1575
1648
  async function assertPrivateWizardConfig(handle) {
@@ -1671,16 +1744,21 @@ async function launchPersonalChrome(opts) {
1671
1744
  if (!safeDataDir || !isAbsolutePath(rawDataDir) || isFilesystemRoot(safeDataDir) || /[\u0000-\u001f\u007f]/.test(rawDataDir)) {
1672
1745
  throw new AppError("INSTALL_CONFIG_INVALID", "The personal Chrome data directory must be an absolute non-root path without control characters.");
1673
1746
  }
1747
+ if (opts.probeAttempts !== void 0 && (!Number.isSafeInteger(opts.probeAttempts) || opts.probeAttempts < 1 || opts.probeAttempts > MAX_PROBE_ATTEMPTS)) {
1748
+ throw new AppError("INSTALL_CONFIG_INVALID", `The personal Chrome probe attempt count must be an integer between 1 and ${MAX_PROBE_ATTEMPTS}.`);
1749
+ }
1750
+ await ensureSecureDirectory(safeDataDir);
1751
+ const personalProfileDir = join7(safeDataDir, "personal-chrome");
1752
+ await ensureSecureDirectory(personalProfileDir);
1674
1753
  const { findChromeExecutable: findChromeExecutable2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
1675
1754
  const executable = opts.executablePath ?? findChromeExecutable2()?.path;
1676
- if (!executable) {
1755
+ if (!executable || !isExecutableFile(executable)) {
1677
1756
  throw new AppError("BROWSER_NOT_CONFIGURED", "Install Chrome or set SMOOTH_OPERATOR_BROWSER_EXECUTABLE");
1678
1757
  }
1679
- await ensureSecureDirectory(safeDataDir);
1680
1758
  const spawnFn = opts.spawn ?? (await import("node:child_process")).spawn;
1681
1759
  const args = [
1682
1760
  `--remote-debugging-port=${port}`,
1683
- `--user-data-dir=${join7(safeDataDir, "personal-chrome")}`,
1761
+ `--user-data-dir=${personalProfileDir}`,
1684
1762
  "--no-first-run",
1685
1763
  "--no-default-browser-check",
1686
1764
  ...opts.headless ? ["--headless=new"] : []
@@ -1689,19 +1767,44 @@ async function launchPersonalChrome(opts) {
1689
1767
  child.unref();
1690
1768
  const probe = opts.probe;
1691
1769
  const attempts = opts.probeAttempts ?? DEFAULT_PROBE_ATTEMPTS;
1770
+ const deadline = opts.probeAttempts === void 0 ? Date.now() + DEFAULT_PROBE_DEADLINE_MS : void 0;
1771
+ let attemptsMade = 0;
1692
1772
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1773
+ if (deadline !== void 0 && Date.now() >= deadline) {
1774
+ break;
1775
+ }
1693
1776
  if (attempt > 0) {
1694
- await new Promise((resolveTimeout) => setTimeout(resolveTimeout, PROBE_INTERVAL_MS));
1777
+ const remaining2 = deadline === void 0 ? PROBE_INTERVAL_MS : deadline - Date.now();
1778
+ if (remaining2 <= 0) break;
1779
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, Math.min(PROBE_INTERVAL_MS, remaining2)));
1695
1780
  }
1781
+ const remaining = deadline === void 0 ? PROBE_TIMEOUT_MS : Math.min(PROBE_TIMEOUT_MS, deadline - Date.now());
1782
+ if (remaining <= 0) break;
1783
+ attemptsMade += 1;
1696
1784
  try {
1697
- const res = await probe(`http://127.0.0.1:${port}/json/version`, 1e3);
1785
+ const res = await boundedProbe(probe, `http://127.0.0.1:${port}/json/version`, remaining);
1698
1786
  if (res.state === "live") return { url: `http://127.0.0.1:${port}` };
1699
1787
  } catch {
1700
1788
  }
1701
1789
  }
1702
- throw new AppError("BROWSER_CONNECT_TIMEOUT", `Chrome DevTools endpoint on port ${port} did not become ready after ${attempts} probes. Close Chrome or choose another port.`);
1790
+ throw new AppError("BROWSER_CONNECT_TIMEOUT", `Chrome DevTools endpoint on port ${port} did not become ready after ${attemptsMade} probes. Close Chrome or choose another port.`);
1791
+ }
1792
+ async function boundedProbe(probe, url, timeoutMs) {
1793
+ let timer;
1794
+ try {
1795
+ return await Promise.race([
1796
+ Promise.resolve().then(() => probe(url, timeoutMs)),
1797
+ new Promise((resolveProbe) => {
1798
+ timer = setTimeout(() => resolveProbe({ state: "timeout" }), timeoutMs);
1799
+ })
1800
+ ]);
1801
+ } finally {
1802
+ if (timer) {
1803
+ clearTimeout(timer);
1804
+ }
1805
+ }
1703
1806
  }
1704
- var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, MAX_WIZARD_CONFIG_BYTES, HARNESS_MENU, WIZARD_STEP_TOTAL;
1807
+ var PROBE_INTERVAL_MS, PROBE_TIMEOUT_MS, DEFAULT_PROBE_ATTEMPTS, DEFAULT_PROBE_DEADLINE_MS, MAX_PROBE_ATTEMPTS, MAX_PROBE_RESPONSE_BYTES, MAX_WIZARD_CONFIG_BYTES, HARNESS_MENU, WIZARD_STEP_TOTAL;
1705
1808
  var init_installer_wizard = __esm({
1706
1809
  "src/server/installer-wizard.ts"() {
1707
1810
  "use strict";
@@ -1710,7 +1813,11 @@ var init_installer_wizard = __esm({
1710
1813
  init_ui();
1711
1814
  init_version();
1712
1815
  PROBE_INTERVAL_MS = 300;
1816
+ PROBE_TIMEOUT_MS = 1e3;
1713
1817
  DEFAULT_PROBE_ATTEMPTS = 33;
1818
+ DEFAULT_PROBE_DEADLINE_MS = 1e4;
1819
+ MAX_PROBE_ATTEMPTS = 100;
1820
+ MAX_PROBE_RESPONSE_BYTES = 64 * 1024;
1714
1821
  MAX_WIZARD_CONFIG_BYTES = 2e6;
1715
1822
  HARNESS_MENU = [
1716
1823
  { id: "opencode", label: "OpenCode", description: "Configures ~/.config/opencode/opencode.json" },
@@ -1734,7 +1841,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
1734
1841
  import process4 from "node:process";
1735
1842
  import { Readable } from "node:stream";
1736
1843
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1737
- import { createMcpHandler } from "@modelcontextprotocol/server";
1844
+ import { createMcpHandler, isJsonContentType as sdkIsJsonContentType } from "@modelcontextprotocol/server";
1738
1845
  import { toNodeHandler } from "@modelcontextprotocol/node";
1739
1846
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
1740
1847
 
@@ -1907,7 +2014,7 @@ function isValidDomainPattern(pattern) {
1907
2014
  }
1908
2015
  const bracketless = base.replace(/^\[|\]$/g, "");
1909
2016
  if (isIP(bracketless) !== 0) {
1910
- return true;
2017
+ return !wildcard;
1911
2018
  }
1912
2019
  const ascii = domainToASCII(base);
1913
2020
  return Boolean(ascii) && ascii.split(".").every((label) => label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
@@ -1999,6 +2106,18 @@ function canonicalizeAllowedFileRoots(rawRoots) {
1999
2106
  if (parse(canonicalRoot).root === canonicalRoot) {
2000
2107
  throw new AppError("CONFIG_INVALID", "Configured file roots must not be filesystem roots.");
2001
2108
  }
2109
+ try {
2110
+ if (!lstatSync(canonicalRoot).isDirectory()) {
2111
+ throw new AppError("CONFIG_INVALID", "Configured file roots must be directories.");
2112
+ }
2113
+ } catch (error) {
2114
+ if (error instanceof AppError) {
2115
+ throw error;
2116
+ }
2117
+ if (!isMissingPathError(error)) {
2118
+ throw new AppError("CONFIG_INSECURE", "Configured file roots could not be inspected safely.", { cause: error });
2119
+ }
2120
+ }
2002
2121
  if (!roots.includes(canonicalRoot)) {
2003
2122
  roots.push(canonicalRoot);
2004
2123
  }
@@ -2312,11 +2431,21 @@ function normalizeHostPattern(value) {
2312
2431
  throw new AppError("CONFIG_INVALID", "Configuration failed validation: configured HTTP host allowlists must contain hostnames or bracketed IPv6 addresses without ports.");
2313
2432
  }
2314
2433
  try {
2315
- return new URL(`http://${trimmed}`).hostname.toLowerCase();
2434
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
2435
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
2316
2436
  } catch (error) {
2317
2437
  throw new AppError("CONFIG_INVALID", "Configuration failed validation: configured HTTP host allowlists contain an invalid hostname.", { cause: error });
2318
2438
  }
2319
2439
  }
2440
+ function normalizeListenHost(value) {
2441
+ const trimmed = value.trim();
2442
+ try {
2443
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
2444
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
2445
+ } catch {
2446
+ return trimmed;
2447
+ }
2448
+ }
2320
2449
  function normalizeHostList(values) {
2321
2450
  return normalizeList(values).map(normalizeHostPattern);
2322
2451
  }
@@ -2329,7 +2458,7 @@ function isValidDomainPattern2(value) {
2329
2458
  }
2330
2459
  const bracketless = base.replace(/^\[|\]$/g, "");
2331
2460
  if (isIP2(bracketless) !== 0) {
2332
- return true;
2461
+ return !wildcard;
2333
2462
  }
2334
2463
  let ascii;
2335
2464
  try {
@@ -2575,7 +2704,7 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
2575
2704
  const config = {
2576
2705
  transport: argValue("--transport") ?? environment.SMOOTH_OPERATOR_TRANSPORT ?? fileConfig.transport ?? "stdio",
2577
2706
  http: {
2578
- host: (argValue("--host") ?? environment.SMOOTH_OPERATOR_HTTP_HOST ?? nestedHttp.host ?? "127.0.0.1").trim(),
2707
+ host: normalizeListenHost(argValue("--host") ?? environment.SMOOTH_OPERATOR_HTTP_HOST ?? nestedHttp.host ?? "127.0.0.1"),
2579
2708
  port: parseInteger(argValue("--port") ?? environment.SMOOTH_OPERATOR_HTTP_PORT, nestedHttp.port ?? 3344),
2580
2709
  path: (environment.SMOOTH_OPERATOR_HTTP_PATH ?? nestedHttp.path ?? "/mcp").trim(),
2581
2710
  token: environment.SMOOTH_OPERATOR_HTTP_TOKEN ?? nestedHttp.token,
@@ -3421,6 +3550,23 @@ var MCP_OUTPUT_TRUNCATION_MARKER_BYTES = UTF8_ENCODER2.encode(MCP_OUTPUT_TRUNCAT
3421
3550
  var MCP_ERROR_CODE_MAX_BYTES = 200;
3422
3551
  var MCP_ERROR_MESSAGE_MAX_BYTES = 4e3;
3423
3552
  var MCP_JSON_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
3553
+ var MCP_OUTPUT_CONTRACT_ARRAY_KEYS = /* @__PURE__ */ new Set([
3554
+ "links",
3555
+ "results",
3556
+ "entries",
3557
+ "interactive",
3558
+ "nodes",
3559
+ "matches",
3560
+ "frames"
3561
+ ]);
3562
+ var MCP_OUTPUT_ARRAY_BOUNDS = [
3563
+ ["links", "linksTruncated"],
3564
+ ["entries", "entriesTruncated"],
3565
+ ["interactive", "interactiveTruncated"],
3566
+ ["nodes", "nodesTruncated"],
3567
+ ["matches", "matchesTruncated"],
3568
+ ["frames", "framesTruncated"]
3569
+ ];
3424
3570
  var NetworkIdleSchema = z3.object({
3425
3571
  timeoutMs: z3.number().int().min(100).max(12e4).optional(),
3426
3572
  pageId: z3.string().trim().min(1).max(200).optional()
@@ -3554,28 +3700,14 @@ var BrowserExecCodeSchema = z3.string().trim().min(1).max(8e4).superRefine((code
3554
3700
  context.addIssue({ code: "custom", message: "code must be a JSON array of validated browser actions." });
3555
3701
  return;
3556
3702
  }
3557
- if (!BrowserActionPlanSchema.safeParse(parsed).success) {
3558
- context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of validated browser actions without nested scripts or screenshots." });
3703
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 100) {
3704
+ context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of at most 100 browser actions." });
3559
3705
  }
3560
3706
  });
3561
3707
  var BrowserExecRequestSchema = z3.object({
3562
3708
  code: BrowserExecCodeSchema,
3563
3709
  confirmDestructive: z3.boolean().optional()
3564
- }).strict().superRefine((input, context) => {
3565
- if (input.confirmDestructive) {
3566
- return;
3567
- }
3568
- let parsed;
3569
- try {
3570
- parsed = JSON.parse(input.code);
3571
- } catch {
3572
- return;
3573
- }
3574
- const actions = BrowserActionPlanSchema.safeParse(parsed);
3575
- if (actions.success && actions.data.some((action) => isDestructiveBatchAction(action.action))) {
3576
- context.addIssue({ code: "custom", path: ["confirmDestructive"], message: "This action plan contains destructive actions. Set confirmDestructive=true to execute them." });
3577
- }
3578
- });
3710
+ }).strict();
3579
3711
  var BrowserUseStateSchema = z3.object({
3580
3712
  include_screenshot: z3.boolean().optional(),
3581
3713
  fullPage: z3.boolean().optional(),
@@ -3615,7 +3747,7 @@ var BROWSER_MUTATING = { ...MUTATING, openWorldHint: true };
3615
3747
  var BROWSER_DESTRUCTIVE = { ...DESTRUCTIVE, openWorldHint: true };
3616
3748
  var MCP_INSTRUCTIONS = [
3617
3749
  "Use browser_snapshot or browser_get_state before interacting so element refs/indexes and viewport coordinates are current.",
3618
- "Serialize dependent browser calls as observe -> one navigation or mutation -> observe. Parallel calls are appropriate only for independent read-only observations; a parallel snapshot and action do not form a transaction.",
3750
+ "Use an observe -> act -> verify loop: serialize dependent browser calls as one navigation or mutation between observations. Parallel calls are appropriate only for independent read-only observations; a parallel snapshot and action do not form a transaction.",
3619
3751
  "Give each request a bounded timeout or cancellation signal. After a timeout or cancellation, inspect current state before retrying a mutation; cancellation is not proof that a mutation did not happen.",
3620
3752
  "After navigation, tab switching, scrolling that changes lazy content, or any DOM-changing action, discard old refs and indexes and capture a fresh snapshot instead of silently falling back to coordinates, text, or a different selector.",
3621
3753
  "Only report titles, URLs, snippets, and metadata that are explicitly present in the returned MCP fields. Absence of a field is evidence of absence: never invent titles, summaries, counts, or other metadata that the tools did not return.",
@@ -3625,8 +3757,7 @@ var MCP_INSTRUCTIONS = [
3625
3757
  "Prefer stable refs, indexes, and selectors over coordinates; use coordinates only when the page cannot expose a reliable target.",
3626
3758
  "For open shadow roots, Puppeteer pierce/ selectors may be used explicitly; closed shadow roots remain unavailable.",
3627
3759
  "Use browser_batch for short validated sequences, but keep destructive actions separate when user confirmation is needed.",
3628
- "Use an efficient observe -> act -> verify loop: observe with browser_snapshot or browser_get_state, perform one bounded browser action, then observe again to verify the resulting state. Keep refs and indexes fresh after navigation, scrolling, or DOM changes; parallelize only independent read-only observations.",
3629
- "browser_solve_challenge is an internal connected-AI loop. Each call is one bounded verification cycle and returns fresh visual/state evidence plus attemptsRemaining; the connected AI should keep using normal browser actions and call it again until the final classification explicitly reports the challenge absent or automation_exhausted. Never claim a challenge is solved from a present, unknown, or failed classification. Human handoff is only an explicit final option after exhaustion.",
3760
+ "browser_solve_challenge is an internal connected-AI loop. Each call is one bounded verification cycle; present and exhausted classifications include fresh visual/state evidence and attemptsRemaining. The connected AI should keep using normal browser actions and call it again until the final classification explicitly reports the challenge absent or automation_exhausted. Never claim a challenge is solved from a present, unknown, or failed classification. Human handoff is only an explicit final option after exhaustion.",
3630
3761
  "The server contains no LLM or agent planner; the MCP client is responsible for reasoning, retries, and task completion."
3631
3762
  ].join(" ");
3632
3763
  function createMcpServer(runtime) {
@@ -3721,7 +3852,10 @@ function registerBrowserTools(server, runtime) {
3721
3852
  inputSchema: BrowserUseExtractSchema,
3722
3853
  annotations: BROWSER_READ_ONLY
3723
3854
  },
3724
- async (input, ctx) => callTool(() => runtime.run({ action: "extract", query: input.query, includeLinks: input.extract_links, pageId: input.pageId, frameId: input.frameId, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime)
3855
+ async (input, ctx) => {
3856
+ const { extract_links, ...fields } = input;
3857
+ return callTool(() => runtime.run({ action: "extract", ...fields, includeLinks: extract_links, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime);
3858
+ }
3725
3859
  );
3726
3860
  registerAction(server, runtime, "browser_navigate", "Navigate the browser", "Open an HTTP(S) URL after domain and private-network policy validation. DNS is checked before navigation but the browser resolver is not pinned. Set includeSnapshot=true for one trailing snapshot.", NavigateRequestSchema, "navigate", (input) => {
3727
3861
  const { new_tab, ...fields } = input;
@@ -3759,7 +3893,10 @@ function registerBrowserTools(server, runtime) {
3759
3893
  { title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
3760
3894
  async (input, ctx) => callTool(() => runtime.run({ action: consoleAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
3761
3895
  );
3762
- registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => ({ ...input, text: input.query }));
3896
+ registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => {
3897
+ const { query, ...fields } = input;
3898
+ return { ...fields, text: query };
3899
+ });
3763
3900
  registerAction(server, runtime, "browser_extract", "Extract page text", "Extract at most 8,000 page-text characters from the page or a CSS selector. Check truncated, offset, nextOffset, hasMore, and revision; use browser_page_next for later slices.", ExtractRequestSchema, "extract", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
3764
3901
  registerAction(server, runtime, "browser_upload", "Upload a file", "Upload a file from an allowed server file root into a file input.", UploadRequestSchema, "upload_file");
3765
3902
  registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
@@ -3778,7 +3915,10 @@ function registerBrowserTools(server, runtime) {
3778
3915
  registerAction(server, runtime, "browser_computed_style", "Read computed style", "Read a small safe subset of computed style for an element.", SelectorRequestSchema, "get_computed_style");
3779
3916
  registerAction(server, runtime, "browser_page_info", "Read page information", "Read URL, title, viewport, and document dimensions.", EmptyInputSchema, "get_page_info");
3780
3917
  registerAction(server, runtime, "browser_hover", "Hover an element", "Move the pointer over a CSS selector or snapshot ref.", TargetRequestSchema, "hover");
3781
- registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) => ({ ...input, coordinateX: input.coordinateX ?? input.coordinate_x, coordinateY: input.coordinateY ?? input.coordinate_y }));
3918
+ registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) => {
3919
+ const { coordinate_x, coordinate_y, ...fields } = input;
3920
+ return { ...fields, coordinateX: fields.coordinateX ?? coordinate_x, coordinateY: fields.coordinateY ?? coordinate_y };
3921
+ });
3782
3922
  registerAction(server, runtime, "browser_press_and_hold", "Press and hold or drag", "Press a mouse button on an element for a bounded duration. Optional startCoordinateX/startCoordinateY and endCoordinateX/endCoordinateY drag with interpolated mouse events; path supplies a bounded explicit pointer path for drawing or selection gestures.", HoldRequestSchema, "press_and_hold");
3783
3923
  registerAction(server, runtime, "browser_challenge", "Detect a web challenge", "Detect bounded challenge markers and return a fresh classification for the current page. A detected challenge is not evidence that it has been solved.", EmptyInputSchema, "detect_challenge");
3784
3924
  registerAction(server, runtime, "browser_wait_for_human", "Wait for human takeover", "Optionally wait for a user to complete a visible challenge or sign-in step in the browser. The result includes a fresh final classification.", WaitForHumanRequestSchema, "wait_for_human");
@@ -3813,11 +3953,24 @@ function registerBrowserTools(server, runtime) {
3813
3953
  "browser_exec",
3814
3954
  {
3815
3955
  title: "Execute a browser action program",
3816
- description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; arbitrary Python or JavaScript is not executed.",
3956
+ description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; it is not a shell or arbitrary Python runner. Page JavaScript is limited to the explicit evaluate action and server policy.",
3817
3957
  inputSchema: BrowserExecRequestSchema,
3818
3958
  annotations: BROWSER_DESTRUCTIVE
3819
3959
  },
3820
- async (input, ctx) => callBatchTool(() => runtime.runBatch(parseBrowserExecCode(input.code), { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal), runtime)
3960
+ async (input, ctx) => callBatchTool(() => {
3961
+ const actions = parseBrowserExecCode(input.code);
3962
+ if (!input.confirmDestructive) {
3963
+ const destructiveIndex = actions.findIndex((action) => isDestructiveBatchAction(action.action));
3964
+ if (destructiveIndex >= 0) {
3965
+ const action = actions[destructiveIndex];
3966
+ throw new AppError("DESTRUCTIVE_CONFIRMATION_REQUIRED", `Action '${action.action}' must be executed separately or with confirmDestructive=true.`, {
3967
+ retryable: true,
3968
+ details: { failedIndex: destructiveIndex, failedAction: action.action, hint: "Set confirmDestructive=true or run the action separately." }
3969
+ });
3970
+ }
3971
+ }
3972
+ return runtime.runBatch(actions, { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal);
3973
+ }, runtime)
3821
3974
  );
3822
3975
  server.registerTool(
3823
3976
  "browser_batch",
@@ -4140,7 +4293,7 @@ function truncateUtf82(value, maxBytes) {
4140
4293
  while (low < high) {
4141
4294
  const midpoint = Math.ceil((low + high) / 2);
4142
4295
  const candidate = decoder.decode(bytes.slice(0, midpoint));
4143
- if (UTF8_ENCODER2.encode(candidate).byteLength <= maxBytes) {
4296
+ if (UTF8_ENCODER2.encode(candidate).byteLength <= boundedMaxBytes) {
4144
4297
  low = midpoint;
4145
4298
  } else {
4146
4299
  high = midpoint - 1;
@@ -4261,9 +4414,8 @@ function boundMcpOutput(value, options = {}) {
4261
4414
  capArray("nodes", MCP_OUTPUT_NODE_LIMIT, "nodesTruncated");
4262
4415
  capArray("matches", MCP_OUTPUT_MATCH_LIMIT, "matchesTruncated");
4263
4416
  capArray("frames", 20, "framesTruncated");
4264
- const contractArrayKeys = /* @__PURE__ */ new Set(["links", "results", "entries", "interactive", "nodes", "matches", "frames"]);
4265
4417
  for (const [key, item] of Object.entries(output)) {
4266
- if (!contractArrayKeys.has(key) && Array.isArray(item)) {
4418
+ if (!MCP_OUTPUT_CONTRACT_ARRAY_KEYS.has(key) && Array.isArray(item)) {
4267
4419
  capArray(key, MCP_OUTPUT_ARRAY_ITEM_LIMIT, `${key}Truncated`);
4268
4420
  }
4269
4421
  }
@@ -4326,10 +4478,7 @@ function boundMcpOutput(value, options = {}) {
4326
4478
  markOutputTruncated();
4327
4479
  }
4328
4480
  }
4329
- const arrayBounds = [["links", "linksTruncated"], ["entries", "entriesTruncated"], ["interactive", "interactiveTruncated"], ["nodes", "nodesTruncated"], ["matches", "matchesTruncated"], ["frames", "framesTruncated"]];
4330
- if (!options.preserveBatchResults) {
4331
- arrayBounds.push(["results", "resultsTruncated"]);
4332
- }
4481
+ const arrayBounds = options.preserveBatchResults ? MCP_OUTPUT_ARRAY_BOUNDS : [...MCP_OUTPUT_ARRAY_BOUNDS, ["results", "resultsTruncated"]];
4333
4482
  for (const [key, flag] of arrayBounds) {
4334
4483
  while (jsonByteLength2(output) > MCP_OUTPUT_MAX_BYTES && Array.isArray(output[key]) && output[key].length > 1) {
4335
4484
  const items = output[key];
@@ -12099,7 +12248,9 @@ var ServerRuntime = class _ServerRuntime {
12099
12248
  classification: "bounded-evidence",
12100
12249
  connectedAiLoop: true,
12101
12250
  humanHandoff: true,
12102
- successRequiresAbsentClassification: true
12251
+ successRequiresAbsentClassification: true,
12252
+ defaultMaxAttempts: 32,
12253
+ maxAttempts: 100
12103
12254
  },
12104
12255
  persistence: {
12105
12256
  fileRootsConfigured: this.config.security.allowedFileRoots.length > 0,
@@ -12212,7 +12363,9 @@ async function reclaimStaleLock(lockPath) {
12212
12363
  if (after.ino !== before.ino || after.dev !== before.dev) {
12213
12364
  return false;
12214
12365
  }
12215
- await rename2(lockPath, `${lockPath}.stale-${randomUUID2()}`);
12366
+ const stalePath = `${lockPath}.stale-${randomUUID2()}`;
12367
+ await rename2(lockPath, stalePath);
12368
+ await unlink2(stalePath).catch(() => void 0);
12216
12369
  return true;
12217
12370
  } catch {
12218
12371
  return false;
@@ -12423,6 +12576,10 @@ var HTTP_NOT_FOUND_BODY = JSON.stringify({ error: "not_found" });
12423
12576
  var HTTP_SHUTTING_DOWN_BODY = JSON.stringify({ error: "server_shutting_down" });
12424
12577
  var HTTP_BUSY_BODY = JSON.stringify({ error: "server_busy" });
12425
12578
  var HTTP_UNAUTHORIZED_BODY = JSON.stringify({ error: "unauthorized" });
12579
+ var HTTP_UNSUPPORTED_MEDIA_BODY = JSON.stringify({
12580
+ jsonrpc: "2.0",
12581
+ error: { code: -32e3, message: "Unsupported Media Type: Content-Type must be application/json" }
12582
+ });
12426
12583
  async function main(args = process4.argv.slice(2)) {
12427
12584
  if (args[0] === "install") {
12428
12585
  const yes = args.includes("--yes") || args.includes("--no-interactive");
@@ -12770,6 +12927,14 @@ async function dispatchHttpRequest(request, response, nodeHandler, maxBodyBytes,
12770
12927
  if (request.aborted) {
12771
12928
  throw new AppError("HTTP_REQUEST_ABORTED", "The HTTP client disconnected before the request completed.", { status: 499, retryable: true });
12772
12929
  }
12930
+ const contentType = request.headers["content-type"];
12931
+ if (request.method?.toUpperCase() === "POST" && (typeof contentType !== "string" || !sdkIsJsonContentType(contentType))) {
12932
+ response.setHeader("connection", "close");
12933
+ closeIncompleteRequestAfterResponse(request, response);
12934
+ response.writeHead(415, { "content-type": "application/json" });
12935
+ response.end(HTTP_UNSUPPORTED_MEDIA_BODY);
12936
+ return;
12937
+ }
12773
12938
  const contentLength = Number(request.headers["content-length"] ?? 0);
12774
12939
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
12775
12940
  closeIncompleteRequestAfterResponse(request, response);