smooth-operator-mcp 3.0.1 → 3.0.2

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.2";
401
401
  }
402
402
  });
403
403
 
@@ -1734,7 +1734,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
1734
1734
  import process4 from "node:process";
1735
1735
  import { Readable } from "node:stream";
1736
1736
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1737
- import { createMcpHandler } from "@modelcontextprotocol/server";
1737
+ import { createMcpHandler, isJsonContentType as sdkIsJsonContentType } from "@modelcontextprotocol/server";
1738
1738
  import { toNodeHandler } from "@modelcontextprotocol/node";
1739
1739
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
1740
1740
 
@@ -3421,6 +3421,23 @@ var MCP_OUTPUT_TRUNCATION_MARKER_BYTES = UTF8_ENCODER2.encode(MCP_OUTPUT_TRUNCAT
3421
3421
  var MCP_ERROR_CODE_MAX_BYTES = 200;
3422
3422
  var MCP_ERROR_MESSAGE_MAX_BYTES = 4e3;
3423
3423
  var MCP_JSON_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
3424
+ var MCP_OUTPUT_CONTRACT_ARRAY_KEYS = /* @__PURE__ */ new Set([
3425
+ "links",
3426
+ "results",
3427
+ "entries",
3428
+ "interactive",
3429
+ "nodes",
3430
+ "matches",
3431
+ "frames"
3432
+ ]);
3433
+ var MCP_OUTPUT_ARRAY_BOUNDS = [
3434
+ ["links", "linksTruncated"],
3435
+ ["entries", "entriesTruncated"],
3436
+ ["interactive", "interactiveTruncated"],
3437
+ ["nodes", "nodesTruncated"],
3438
+ ["matches", "matchesTruncated"],
3439
+ ["frames", "framesTruncated"]
3440
+ ];
3424
3441
  var NetworkIdleSchema = z3.object({
3425
3442
  timeoutMs: z3.number().int().min(100).max(12e4).optional(),
3426
3443
  pageId: z3.string().trim().min(1).max(200).optional()
@@ -3554,28 +3571,14 @@ var BrowserExecCodeSchema = z3.string().trim().min(1).max(8e4).superRefine((code
3554
3571
  context.addIssue({ code: "custom", message: "code must be a JSON array of validated browser actions." });
3555
3572
  return;
3556
3573
  }
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." });
3574
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 100) {
3575
+ context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of at most 100 browser actions." });
3559
3576
  }
3560
3577
  });
3561
3578
  var BrowserExecRequestSchema = z3.object({
3562
3579
  code: BrowserExecCodeSchema,
3563
3580
  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
- });
3581
+ }).strict();
3579
3582
  var BrowserUseStateSchema = z3.object({
3580
3583
  include_screenshot: z3.boolean().optional(),
3581
3584
  fullPage: z3.boolean().optional(),
@@ -3615,7 +3618,7 @@ var BROWSER_MUTATING = { ...MUTATING, openWorldHint: true };
3615
3618
  var BROWSER_DESTRUCTIVE = { ...DESTRUCTIVE, openWorldHint: true };
3616
3619
  var MCP_INSTRUCTIONS = [
3617
3620
  "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.",
3621
+ "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
3622
  "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
3623
  "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
3624
  "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 +3628,7 @@ var MCP_INSTRUCTIONS = [
3625
3628
  "Prefer stable refs, indexes, and selectors over coordinates; use coordinates only when the page cannot expose a reliable target.",
3626
3629
  "For open shadow roots, Puppeteer pierce/ selectors may be used explicitly; closed shadow roots remain unavailable.",
3627
3630
  "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.",
3631
+ "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
3632
  "The server contains no LLM or agent planner; the MCP client is responsible for reasoning, retries, and task completion."
3631
3633
  ].join(" ");
3632
3634
  function createMcpServer(runtime) {
@@ -3721,7 +3723,10 @@ function registerBrowserTools(server, runtime) {
3721
3723
  inputSchema: BrowserUseExtractSchema,
3722
3724
  annotations: BROWSER_READ_ONLY
3723
3725
  },
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)
3726
+ async (input, ctx) => {
3727
+ const { extract_links, ...fields } = input;
3728
+ return callTool(() => runtime.run({ action: "extract", ...fields, includeLinks: extract_links, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime);
3729
+ }
3725
3730
  );
3726
3731
  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
3732
  const { new_tab, ...fields } = input;
@@ -3759,7 +3764,10 @@ function registerBrowserTools(server, runtime) {
3759
3764
  { title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
3760
3765
  async (input, ctx) => callTool(() => runtime.run({ action: consoleAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
3761
3766
  );
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 }));
3767
+ registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => {
3768
+ const { query, ...fields } = input;
3769
+ return { ...fields, text: query };
3770
+ });
3763
3771
  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
3772
  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
3773
  registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
@@ -3778,7 +3786,10 @@ function registerBrowserTools(server, runtime) {
3778
3786
  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
3787
  registerAction(server, runtime, "browser_page_info", "Read page information", "Read URL, title, viewport, and document dimensions.", EmptyInputSchema, "get_page_info");
3780
3788
  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 }));
3789
+ 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) => {
3790
+ const { coordinate_x, coordinate_y, ...fields } = input;
3791
+ return { ...fields, coordinateX: fields.coordinateX ?? coordinate_x, coordinateY: fields.coordinateY ?? coordinate_y };
3792
+ });
3782
3793
  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
3794
  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
3795
  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 +3824,24 @@ function registerBrowserTools(server, runtime) {
3813
3824
  "browser_exec",
3814
3825
  {
3815
3826
  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.",
3827
+ 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
3828
  inputSchema: BrowserExecRequestSchema,
3818
3829
  annotations: BROWSER_DESTRUCTIVE
3819
3830
  },
3820
- async (input, ctx) => callBatchTool(() => runtime.runBatch(parseBrowserExecCode(input.code), { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal), runtime)
3831
+ async (input, ctx) => callBatchTool(() => {
3832
+ const actions = parseBrowserExecCode(input.code);
3833
+ if (!input.confirmDestructive) {
3834
+ const destructiveIndex = actions.findIndex((action) => isDestructiveBatchAction(action.action));
3835
+ if (destructiveIndex >= 0) {
3836
+ const action = actions[destructiveIndex];
3837
+ throw new AppError("DESTRUCTIVE_CONFIRMATION_REQUIRED", `Action '${action.action}' must be executed separately or with confirmDestructive=true.`, {
3838
+ retryable: true,
3839
+ details: { failedIndex: destructiveIndex, failedAction: action.action, hint: "Set confirmDestructive=true or run the action separately." }
3840
+ });
3841
+ }
3842
+ }
3843
+ return runtime.runBatch(actions, { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal);
3844
+ }, runtime)
3821
3845
  );
3822
3846
  server.registerTool(
3823
3847
  "browser_batch",
@@ -4140,7 +4164,7 @@ function truncateUtf82(value, maxBytes) {
4140
4164
  while (low < high) {
4141
4165
  const midpoint = Math.ceil((low + high) / 2);
4142
4166
  const candidate = decoder.decode(bytes.slice(0, midpoint));
4143
- if (UTF8_ENCODER2.encode(candidate).byteLength <= maxBytes) {
4167
+ if (UTF8_ENCODER2.encode(candidate).byteLength <= boundedMaxBytes) {
4144
4168
  low = midpoint;
4145
4169
  } else {
4146
4170
  high = midpoint - 1;
@@ -4261,9 +4285,8 @@ function boundMcpOutput(value, options = {}) {
4261
4285
  capArray("nodes", MCP_OUTPUT_NODE_LIMIT, "nodesTruncated");
4262
4286
  capArray("matches", MCP_OUTPUT_MATCH_LIMIT, "matchesTruncated");
4263
4287
  capArray("frames", 20, "framesTruncated");
4264
- const contractArrayKeys = /* @__PURE__ */ new Set(["links", "results", "entries", "interactive", "nodes", "matches", "frames"]);
4265
4288
  for (const [key, item] of Object.entries(output)) {
4266
- if (!contractArrayKeys.has(key) && Array.isArray(item)) {
4289
+ if (!MCP_OUTPUT_CONTRACT_ARRAY_KEYS.has(key) && Array.isArray(item)) {
4267
4290
  capArray(key, MCP_OUTPUT_ARRAY_ITEM_LIMIT, `${key}Truncated`);
4268
4291
  }
4269
4292
  }
@@ -4326,10 +4349,7 @@ function boundMcpOutput(value, options = {}) {
4326
4349
  markOutputTruncated();
4327
4350
  }
4328
4351
  }
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
- }
4352
+ const arrayBounds = options.preserveBatchResults ? MCP_OUTPUT_ARRAY_BOUNDS : [...MCP_OUTPUT_ARRAY_BOUNDS, ["results", "resultsTruncated"]];
4333
4353
  for (const [key, flag] of arrayBounds) {
4334
4354
  while (jsonByteLength2(output) > MCP_OUTPUT_MAX_BYTES && Array.isArray(output[key]) && output[key].length > 1) {
4335
4355
  const items = output[key];
@@ -12099,7 +12119,9 @@ var ServerRuntime = class _ServerRuntime {
12099
12119
  classification: "bounded-evidence",
12100
12120
  connectedAiLoop: true,
12101
12121
  humanHandoff: true,
12102
- successRequiresAbsentClassification: true
12122
+ successRequiresAbsentClassification: true,
12123
+ defaultMaxAttempts: 32,
12124
+ maxAttempts: 100
12103
12125
  },
12104
12126
  persistence: {
12105
12127
  fileRootsConfigured: this.config.security.allowedFileRoots.length > 0,
@@ -12212,7 +12234,9 @@ async function reclaimStaleLock(lockPath) {
12212
12234
  if (after.ino !== before.ino || after.dev !== before.dev) {
12213
12235
  return false;
12214
12236
  }
12215
- await rename2(lockPath, `${lockPath}.stale-${randomUUID2()}`);
12237
+ const stalePath = `${lockPath}.stale-${randomUUID2()}`;
12238
+ await rename2(lockPath, stalePath);
12239
+ await unlink2(stalePath).catch(() => void 0);
12216
12240
  return true;
12217
12241
  } catch {
12218
12242
  return false;
@@ -12423,6 +12447,10 @@ var HTTP_NOT_FOUND_BODY = JSON.stringify({ error: "not_found" });
12423
12447
  var HTTP_SHUTTING_DOWN_BODY = JSON.stringify({ error: "server_shutting_down" });
12424
12448
  var HTTP_BUSY_BODY = JSON.stringify({ error: "server_busy" });
12425
12449
  var HTTP_UNAUTHORIZED_BODY = JSON.stringify({ error: "unauthorized" });
12450
+ var HTTP_UNSUPPORTED_MEDIA_BODY = JSON.stringify({
12451
+ jsonrpc: "2.0",
12452
+ error: { code: -32e3, message: "Unsupported Media Type: Content-Type must be application/json" }
12453
+ });
12426
12454
  async function main(args = process4.argv.slice(2)) {
12427
12455
  if (args[0] === "install") {
12428
12456
  const yes = args.includes("--yes") || args.includes("--no-interactive");
@@ -12770,6 +12798,14 @@ async function dispatchHttpRequest(request, response, nodeHandler, maxBodyBytes,
12770
12798
  if (request.aborted) {
12771
12799
  throw new AppError("HTTP_REQUEST_ABORTED", "The HTTP client disconnected before the request completed.", { status: 499, retryable: true });
12772
12800
  }
12801
+ const contentType = request.headers["content-type"];
12802
+ if (request.method?.toUpperCase() === "POST" && (typeof contentType !== "string" || !sdkIsJsonContentType(contentType))) {
12803
+ response.setHeader("connection", "close");
12804
+ closeIncompleteRequestAfterResponse(request, response);
12805
+ response.writeHead(415, { "content-type": "application/json" });
12806
+ response.end(HTTP_UNSUPPORTED_MEDIA_BODY);
12807
+ return;
12808
+ }
12773
12809
  const contentLength = Number(request.headers["content-length"] ?? 0);
12774
12810
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
12775
12811
  closeIncompleteRequestAfterResponse(request, response);