smooth-operator-mcp 2.4.6 → 2.4.7
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/.env.example +3 -0
- package/README.md +2 -0
- package/dist/smooth-operator.mjs +54 -18
- package/dist/smooth-operator.mjs.map +3 -3
- package/docs/harnesses.md +14 -0
- package/docs/mcp-server.md +54 -8
- package/package.json +3 -3
package/.env.example
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Native MCP transport
|
|
2
2
|
SMOOTH_OPERATOR_TRANSPORT=stdio
|
|
3
3
|
SMOOTH_OPERATOR_DATA_DIR=~/.smooth-operator
|
|
4
|
+
# Optional explicit JSON config path. The installer-created
|
|
5
|
+
# ~/.smooth-operator/config.json is loaded automatically when this is unset.
|
|
6
|
+
# SMOOTH_OPERATOR_CONFIG=~/.smooth-operator/config.json
|
|
4
7
|
|
|
5
8
|
# Managed agent Chrome is the zero-setup default. It discovers Google Chrome,
|
|
6
9
|
# then creates one private persistent profile under SMOOTH_OPERATOR_DATA_DIR.
|
package/README.md
CHANGED
|
@@ -54,3 +54,5 @@ Talk to your harness normally. It will call `browser_navigate` → `browser_snap
|
|
|
54
54
|
- Works — stdio default, HTTP optional
|
|
55
55
|
|
|
56
56
|
Details: [AGENTS.md](AGENTS.md) · [MCP guide](docs/mcp-server.md) · [harnesses](docs/harnesses.md)
|
|
57
|
+
|
|
58
|
+
ChatGPT developer-mode connections use the private [Secure MCP Tunnel](docs/mcp-server.md#chatgpt-and-openai-connections) route or a separately secured HTTPS deployment.
|
package/dist/smooth-operator.mjs
CHANGED
|
@@ -283,6 +283,15 @@ var init_errors = __esm({
|
|
|
283
283
|
}
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
// src/server/version.ts
|
|
287
|
+
var SERVER_VERSION;
|
|
288
|
+
var init_version = __esm({
|
|
289
|
+
"src/server/version.ts"() {
|
|
290
|
+
"use strict";
|
|
291
|
+
SERVER_VERSION = "2.4.7";
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
286
295
|
// src/server/browser/discovery.ts
|
|
287
296
|
var discovery_exports = {};
|
|
288
297
|
__export(discovery_exports, {
|
|
@@ -608,11 +617,11 @@ async function readConfigFile2(path) {
|
|
|
608
617
|
if (isMissingFile2(error)) {
|
|
609
618
|
return void 0;
|
|
610
619
|
}
|
|
611
|
-
if (noFollow && (
|
|
620
|
+
if (noFollow && (isErrorCode2(error, "EINVAL") || isErrorCode2(error, "ENOTSUP") || isErrorCode2(error, "EOPNOTSUPP"))) {
|
|
612
621
|
await rejectSymlink2(path, "configuration file");
|
|
613
622
|
handle = await open3(path, constants2.O_RDONLY);
|
|
614
623
|
} else {
|
|
615
|
-
if (
|
|
624
|
+
if (isErrorCode2(error, "ELOOP") || isErrorCode2(error, "EFTYPE")) {
|
|
616
625
|
throw new AppError("INSTALL_CONFIG_FAILED", `The configuration file '${path}' must not be a symbolic link.`);
|
|
617
626
|
}
|
|
618
627
|
throw error;
|
|
@@ -804,7 +813,7 @@ async function createUniqueBackup(path, reviewedBytes) {
|
|
|
804
813
|
await handle.sync();
|
|
805
814
|
} catch (error) {
|
|
806
815
|
await handle?.close().catch(() => void 0);
|
|
807
|
-
if (
|
|
816
|
+
if (isErrorCode2(error, "EEXIST")) {
|
|
808
817
|
continue;
|
|
809
818
|
}
|
|
810
819
|
throw new AppError("INSTALL_BACKUP_FAILED", `Could not create a backup before updating ${path}.`, { cause: error });
|
|
@@ -930,9 +939,9 @@ function isRecord3(value) {
|
|
|
930
939
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
931
940
|
}
|
|
932
941
|
function isMissingFile2(error) {
|
|
933
|
-
return
|
|
942
|
+
return isErrorCode2(error, "ENOENT");
|
|
934
943
|
}
|
|
935
|
-
function
|
|
944
|
+
function isErrorCode2(error, code) {
|
|
936
945
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
937
946
|
}
|
|
938
947
|
function truncate(value, maxBytes) {
|
|
@@ -1211,7 +1220,7 @@ async function runWizard(harness, opts) {
|
|
|
1211
1220
|
});
|
|
1212
1221
|
const session = tolerantQuestion(rl);
|
|
1213
1222
|
try {
|
|
1214
|
-
ui.banner("SmoothOperator Setup", `Give ${harness} a real Chrome it can drive`, opts.version ??
|
|
1223
|
+
ui.banner("SmoothOperator Setup", `Give ${harness} a real Chrome it can drive`, opts.version ?? SERVER_VERSION);
|
|
1215
1224
|
ui.note(`Configuring: ${harness}`);
|
|
1216
1225
|
ui.note("Answer each question, or press Enter to accept the recommended default.");
|
|
1217
1226
|
ui.note(`You can re-run \`smooth-operator install ${harness}\` at any time to change these.`);
|
|
@@ -1476,6 +1485,7 @@ var init_installer_wizard = __esm({
|
|
|
1476
1485
|
"src/server/installer-wizard.ts"() {
|
|
1477
1486
|
"use strict";
|
|
1478
1487
|
init_ui();
|
|
1488
|
+
init_version();
|
|
1479
1489
|
PROBE_INTERVAL_MS = 300;
|
|
1480
1490
|
DEFAULT_PROBE_ATTEMPTS = 33;
|
|
1481
1491
|
HARNESS_MENU = [
|
|
@@ -1600,7 +1610,10 @@ function trimOptional(value) {
|
|
|
1600
1610
|
function expandOptionalPath(value) {
|
|
1601
1611
|
return value === void 0 ? void 0 : expandPath(value);
|
|
1602
1612
|
}
|
|
1603
|
-
function
|
|
1613
|
+
function isErrorCode(error, code) {
|
|
1614
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
1615
|
+
}
|
|
1616
|
+
function readConfigFile(configPath, options = {}) {
|
|
1604
1617
|
let descriptor;
|
|
1605
1618
|
try {
|
|
1606
1619
|
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
@@ -1628,7 +1641,8 @@ function readConfigFile(configPath) {
|
|
|
1628
1641
|
}
|
|
1629
1642
|
}
|
|
1630
1643
|
const parsed = JSON.parse(readFileSync(descriptor, "utf8"));
|
|
1631
|
-
const
|
|
1644
|
+
const schema = options.allowUnknownRootKeys ? RawConfigSchema.strip() : RawConfigSchema;
|
|
1645
|
+
const result = schema.safeParse(parsed);
|
|
1632
1646
|
if (!result.success) {
|
|
1633
1647
|
throw new AppError("CONFIG_INVALID", "Configuration file failed schema validation.", {
|
|
1634
1648
|
details: { issues: result.error.issues.map((issue) => issue.message) }
|
|
@@ -1639,6 +1653,9 @@ function readConfigFile(configPath) {
|
|
|
1639
1653
|
if (error instanceof AppError) {
|
|
1640
1654
|
throw error;
|
|
1641
1655
|
}
|
|
1656
|
+
if (options.allowMissing && isErrorCode(error, "ENOENT")) {
|
|
1657
|
+
return {};
|
|
1658
|
+
}
|
|
1642
1659
|
if (error && typeof error === "object" && "code" in error && error.code === "ELOOP") {
|
|
1643
1660
|
throw new AppError("CONFIG_INSECURE", "Configuration files must not be symbolic links.", { cause: error });
|
|
1644
1661
|
}
|
|
@@ -1692,7 +1709,7 @@ function validateConfig(config) {
|
|
|
1692
1709
|
}
|
|
1693
1710
|
return config;
|
|
1694
1711
|
}
|
|
1695
|
-
function loadServerConfig(args = [], environment = env) {
|
|
1712
|
+
function loadServerConfig(args = [], environment = env, homeDirectory = homedir()) {
|
|
1696
1713
|
if (environment.SMOOTH_OPERATOR_BROWSER_PROFILE !== void 0 || environment.SMOOTH_OPERATOR_BROWSER_STEALTH !== void 0) {
|
|
1697
1714
|
throw new AppError("CONFIG_INVALID", "Browser profile switches were removed. The native server uses one fixed native profile.");
|
|
1698
1715
|
}
|
|
@@ -1705,11 +1722,12 @@ function loadServerConfig(args = [], environment = env) {
|
|
|
1705
1722
|
const argumentValues = parseArguments(args);
|
|
1706
1723
|
const argValue = (name) => argumentValues.get(name);
|
|
1707
1724
|
const configPath = argValue("--config") ?? environment.SMOOTH_OPERATOR_CONFIG;
|
|
1708
|
-
const
|
|
1725
|
+
const defaultConfigPath = join(homeDirectory, ".smooth-operator", "config.json");
|
|
1726
|
+
const fileConfig = configPath ? readConfigFile(expandPath(configPath)) : readConfigFile(defaultConfigPath, { allowMissing: true, allowUnknownRootKeys: true });
|
|
1709
1727
|
const nestedHttp = fileConfig.http ?? {};
|
|
1710
1728
|
const nestedBrowser = fileConfig.browser ?? {};
|
|
1711
1729
|
const nestedSecurity = fileConfig.security ?? {};
|
|
1712
|
-
const dataDir = expandPath(environment.SMOOTH_OPERATOR_DATA_DIR ?? fileConfig.dataDir ?? join(
|
|
1730
|
+
const dataDir = expandPath(environment.SMOOTH_OPERATOR_DATA_DIR ?? fileConfig.dataDir ?? join(homeDirectory, ".smooth-operator"));
|
|
1713
1731
|
const defaultBrowserDataDir = join(dataDir, "browser");
|
|
1714
1732
|
const configuredRoots = parseList(environment.SMOOTH_OPERATOR_ALLOWED_FILE_ROOTS, nestedSecurity.allowedFileRoots ?? []);
|
|
1715
1733
|
const allowedFileRoots = (configuredRoots.length > 0 ? configuredRoots : [join(dataDir, "files"), join(dataDir, "downloads")]).map(expandPath);
|
|
@@ -2443,11 +2461,7 @@ function isDestructiveBatchAction(action) {
|
|
|
2443
2461
|
// src/server/mcp.ts
|
|
2444
2462
|
init_errors();
|
|
2445
2463
|
init_logger();
|
|
2446
|
-
|
|
2447
|
-
// src/server/version.ts
|
|
2448
|
-
var SERVER_VERSION = "2.4.6";
|
|
2449
|
-
|
|
2450
|
-
// src/server/mcp.ts
|
|
2464
|
+
init_version();
|
|
2451
2465
|
var EmptyInputSchema = z3.object({}).strict();
|
|
2452
2466
|
var ActionEmptyInputSchema = z3.object({ includeSnapshot: z3.boolean().optional() }).strict();
|
|
2453
2467
|
var MCP_OUTPUT_MAX_BYTES = 28e3;
|
|
@@ -8512,6 +8526,7 @@ async function readBoundedResponseText(response, maxBytes) {
|
|
|
8512
8526
|
}
|
|
8513
8527
|
|
|
8514
8528
|
// src/server/runtime.ts
|
|
8529
|
+
init_version();
|
|
8515
8530
|
var ServerRuntime = class _ServerRuntime {
|
|
8516
8531
|
constructor(config, browserProfileLease) {
|
|
8517
8532
|
this.config = config;
|
|
@@ -8920,6 +8935,7 @@ async function runShutdownPhase(label, operation, timeoutMs, logger) {
|
|
|
8920
8935
|
|
|
8921
8936
|
// src/server/main.ts
|
|
8922
8937
|
init_installer();
|
|
8938
|
+
init_version();
|
|
8923
8939
|
import { homedir as homedir5 } from "node:os";
|
|
8924
8940
|
var INSTALL_USAGE = `Usage: smooth-operator install [harness] (interactive when no harness is given)
|
|
8925
8941
|
harness: <${supportedHarnessTargets().join("|")}>
|
|
@@ -9097,11 +9113,22 @@ async function serveHttp(runtime, shutdown) {
|
|
|
9097
9113
|
if (!validateHost(request, response) || !validateOrigin(request, response)) {
|
|
9098
9114
|
return;
|
|
9099
9115
|
}
|
|
9116
|
+
setCorsHeaders(request, response);
|
|
9100
9117
|
if (!requestPathMatches(request, config.http.path)) {
|
|
9101
9118
|
response.writeHead(404, { "content-type": "application/json" });
|
|
9102
9119
|
response.end(JSON.stringify({ error: "not_found" }));
|
|
9103
9120
|
return;
|
|
9104
9121
|
}
|
|
9122
|
+
if (request.method === "OPTIONS") {
|
|
9123
|
+
response.writeHead(204, {
|
|
9124
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
9125
|
+
"access-control-allow-headers": request.headers["access-control-request-headers"] ?? "authorization, content-type, accept, mcp-protocol-version, mcp-session-id, last-event-id",
|
|
9126
|
+
"access-control-expose-headers": "Mcp-Session-Id, WWW-Authenticate",
|
|
9127
|
+
"access-control-max-age": "600"
|
|
9128
|
+
});
|
|
9129
|
+
response.end();
|
|
9130
|
+
return;
|
|
9131
|
+
}
|
|
9105
9132
|
if (!authorized(request, config.http.token)) {
|
|
9106
9133
|
response.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
|
|
9107
9134
|
response.end(JSON.stringify({ error: "unauthorized" }));
|
|
@@ -9212,6 +9239,14 @@ function requestPathMatches(request, expectedPath) {
|
|
|
9212
9239
|
return false;
|
|
9213
9240
|
}
|
|
9214
9241
|
}
|
|
9242
|
+
function setCorsHeaders(request, response) {
|
|
9243
|
+
const origin = request.headers.origin;
|
|
9244
|
+
if (!origin || Array.isArray(origin)) {
|
|
9245
|
+
return;
|
|
9246
|
+
}
|
|
9247
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
9248
|
+
response.setHeader("vary", "Origin");
|
|
9249
|
+
}
|
|
9215
9250
|
async function dispatchHttpRequest(request, response, nodeHandler, maxBodyBytes, promoteToStream) {
|
|
9216
9251
|
if (request.aborted) {
|
|
9217
9252
|
throw new AppError("HTTP_REQUEST_ABORTED", "The HTTP client disconnected before the request completed.", { status: 499, retryable: true });
|
|
@@ -9317,10 +9352,11 @@ function authorized(request, token) {
|
|
|
9317
9352
|
return true;
|
|
9318
9353
|
}
|
|
9319
9354
|
const header = request.headers.authorization;
|
|
9320
|
-
|
|
9355
|
+
const match = header?.match(/^Bearer[ \t]+(.+)$/i);
|
|
9356
|
+
if (!match) {
|
|
9321
9357
|
return false;
|
|
9322
9358
|
}
|
|
9323
|
-
const presented = Buffer.from(
|
|
9359
|
+
const presented = Buffer.from(match[1]);
|
|
9324
9360
|
const expected = createHash("sha256").update(token).digest();
|
|
9325
9361
|
const presentedDigest = createHash("sha256").update(presented).digest();
|
|
9326
9362
|
return presentedDigest.length === expected.length && timingSafeEqual(presentedDigest, expected);
|