smooth-operator-mcp 2.4.5 → 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 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.
@@ -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 && (isErrorCode(error, "EINVAL") || isErrorCode(error, "ENOTSUP") || isErrorCode(error, "EOPNOTSUPP"))) {
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 (isErrorCode(error, "ELOOP") || isErrorCode(error, "EFTYPE")) {
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 (isErrorCode(error, "EEXIST")) {
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 isErrorCode(error, "ENOENT");
942
+ return isErrorCode2(error, "ENOENT");
934
943
  }
935
- function isErrorCode(error, code) {
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 ?? "2.4.5");
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 readConfigFile(configPath) {
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 result = RawConfigSchema.safeParse(parsed);
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 fileConfig = configPath ? readConfigFile(expandPath(configPath)) : {};
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(homedir(), ".smooth-operator"));
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.5";
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
- if (!header?.startsWith("Bearer ")) {
9355
+ const match = header?.match(/^Bearer[ \t]+(.+)$/i);
9356
+ if (!match) {
9321
9357
  return false;
9322
9358
  }
9323
- const presented = Buffer.from(header.slice("Bearer ".length));
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);