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.
- package/dist/smooth-operator.mjs +72 -36
- package/dist/smooth-operator.mjs.map +1 -1
- package/docs/mcp-server.md +12 -6
- 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.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 (!
|
|
3558
|
-
context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of
|
|
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()
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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(() =>
|
|
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 <=
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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);
|