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.
- package/dist/smooth-operator.mjs +225 -60
- package/dist/smooth-operator.mjs.map +2 -2
- package/docs/harnesses.md +7 -6
- package/docs/mcp-server.md +15 -8
- package/package.json +1 -1
package/dist/smooth-operator.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
642
|
+
return join6(resolveConfigDirectory(environment.APPDATA, join6(home, "AppData", "Roaming")), "Claude", "claude_desktop_config.json");
|
|
643
643
|
}
|
|
644
|
-
return join6(environment.XDG_CONFIG_HOME
|
|
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
|
|
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 {
|
|
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) &&
|
|
1317
|
-
ui.failure("Enter a listed number or an existing
|
|
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 (!
|
|
1328
|
-
ui.failure("That path
|
|
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.
|
|
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=${
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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")
|
|
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 (!
|
|
3558
|
-
context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of
|
|
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()
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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(() =>
|
|
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 <=
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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);
|